From 4817660036f2bd133986a912309ede9f172d3179 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Sat, 16 Dec 2023 21:12:25 +0000 Subject: [PATCH 001/121] Update Sire version. [ci skip] --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index f7bb52604..81dcc0ce2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ # BioSimSpace runtime requirements. # main -sire~=2023.5.0 +#sire~=2023.5.0 # devel -#sire==2023.5.0.dev +sire==2024.1.0.dev configargparse ipywidgets From dc348b1911eca412e2881857b39a59521fa425b6 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 18 Jan 2024 09:19:52 +0000 Subject: [PATCH 002/121] Fix path to links file. [closes #231] --- python/BioSimSpace/Align/_align.py | 7 +++++-- python/BioSimSpace/Sandpit/Exscientia/Align/_align.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/python/BioSimSpace/Align/_align.py b/python/BioSimSpace/Align/_align.py index cd0bb9a2c..4713693c9 100644 --- a/python/BioSimSpace/Align/_align.py +++ b/python/BioSimSpace/Align/_align.py @@ -394,10 +394,13 @@ def generateNetwork( if len(records) > 2: new_line += " " + " ".join(records[2:]) + # Store the path to the new file. + lf = f"{work_dir}/inputs/lomap_links_file.txt" + # Write the updated lomap links file. - with open(f"{work_dir}/inputs/lomap_links_file.txt", "w") as lf: + with open(lf, "w") as f: for line in new_lines: - lf.write(line) + f.write(line) else: lf = None diff --git a/python/BioSimSpace/Sandpit/Exscientia/Align/_align.py b/python/BioSimSpace/Sandpit/Exscientia/Align/_align.py index 1ae4ffa92..01b1001e9 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Align/_align.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Align/_align.py @@ -394,10 +394,13 @@ def generateNetwork( if len(records) > 2: new_line += " " + " ".join(records[2:]) + # Store the path to the new file. + lf = f"{work_dir}/inputs/lomap_links_file.txt" + # Write the updated lomap links file. - with open(f"{work_dir}/inputs/lomap_links_file.txt", "w") as lf: + with open(lf, "w") as f: for line in new_lines: - lf.write(line) + f.write(line) else: lf = None From e0b4007ed334086745847a510bef05440ff02e7c Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 18 Jan 2024 09:35:22 +0000 Subject: [PATCH 003/121] Fix redirection of stderr for logger. [ref #232] --- python/BioSimSpace/Parameters/_Protocol/_amber.py | 5 +++-- .../BioSimSpace/Parameters/_Protocol/_openforcefield.py | 5 +++-- .../Sandpit/Exscientia/Parameters/_Protocol/_amber.py | 5 +++-- .../Exscientia/Parameters/_Protocol/_openforcefield.py | 5 +++-- .../Sandpit/Exscientia/Trajectory/_trajectory.py | 1 - python/BioSimSpace/Sandpit/Exscientia/__init__.py | 8 -------- python/BioSimSpace/Trajectory/_trajectory.py | 1 - python/BioSimSpace/__init__.py | 8 -------- 8 files changed, 12 insertions(+), 26 deletions(-) diff --git a/python/BioSimSpace/Parameters/_Protocol/_amber.py b/python/BioSimSpace/Parameters/_Protocol/_amber.py index 5b1601686..0b05329a2 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_amber.py +++ b/python/BioSimSpace/Parameters/_Protocol/_amber.py @@ -44,6 +44,7 @@ # Temporarily redirect stderr to suppress import warnings. import sys as _sys +_orig_stderr = _sys.stderr _sys.stderr = open(_os.devnull, "w") _openff = _try_import("openff") @@ -54,8 +55,8 @@ _OpenFFMolecule = _openff # Reset stderr. -_sys.stderr = _sys.__stderr__ -del _sys +_sys.stderr = _orig_stderr +del _sys, _orig_stderr from sire.legacy import IO as _SireIO from sire.legacy import Mol as _SireMol diff --git a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py index 35882a12c..a30b7ab3e 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py @@ -62,6 +62,7 @@ import sys as _sys # Temporarily redirect stderr to suppress import warnings. +_orig_stderr = _sys.stderr _sys.stderr = open(_os.devnull, "w") _openmm = _try_import("openmm") @@ -85,8 +86,8 @@ _Forcefield = _openff # Reset stderr. -_sys.stderr = _sys.__stderr__ -del _sys +_sys.stderr = _orig_stderr +del _sys, _orig_stderr from sire.legacy import IO as _SireIO from sire.legacy import Mol as _SireMol diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py index 5b1601686..0b05329a2 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py @@ -44,6 +44,7 @@ # Temporarily redirect stderr to suppress import warnings. import sys as _sys +_orig_stderr = _sys.stderr _sys.stderr = open(_os.devnull, "w") _openff = _try_import("openff") @@ -54,8 +55,8 @@ _OpenFFMolecule = _openff # Reset stderr. -_sys.stderr = _sys.__stderr__ -del _sys +_sys.stderr = _orig_stderr +del _sys, _orig_stderr from sire.legacy import IO as _SireIO from sire.legacy import Mol as _SireMol diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py index 35882a12c..a30b7ab3e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py @@ -62,6 +62,7 @@ import sys as _sys # Temporarily redirect stderr to suppress import warnings. +_orig_stderr = _sys.stderr _sys.stderr = open(_os.devnull, "w") _openmm = _try_import("openmm") @@ -85,8 +86,8 @@ _Forcefield = _openff # Reset stderr. -_sys.stderr = _sys.__stderr__ -del _sys +_sys.stderr = _orig_stderr +del _sys, _orig_stderr from sire.legacy import IO as _SireIO from sire.legacy import Mol as _SireMol diff --git a/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py b/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py index 1dc7e7d24..2332e74f6 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py @@ -32,7 +32,6 @@ _mdtraj = _try_import("mdtraj") import copy as _copy -import logging as _logging import os as _os import shutil as _shutil import uuid as _uuid diff --git a/python/BioSimSpace/Sandpit/Exscientia/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/__init__.py index 60edd670b..d99b13fb7 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/__init__.py @@ -248,11 +248,3 @@ def _isVerbose(): from ... import _version __version__ = _version.get_versions()["version"] - -import logging as _logging - -for _name, _logger in _logging.root.manager.loggerDict.items(): - _logger.disabled = True -del _logger -del _logging -del _name diff --git a/python/BioSimSpace/Trajectory/_trajectory.py b/python/BioSimSpace/Trajectory/_trajectory.py index 1dc7e7d24..2332e74f6 100644 --- a/python/BioSimSpace/Trajectory/_trajectory.py +++ b/python/BioSimSpace/Trajectory/_trajectory.py @@ -32,7 +32,6 @@ _mdtraj = _try_import("mdtraj") import copy as _copy -import logging as _logging import os as _os import shutil as _shutil import uuid as _uuid diff --git a/python/BioSimSpace/__init__.py b/python/BioSimSpace/__init__.py index 4d5c2eab5..92fe48f0b 100644 --- a/python/BioSimSpace/__init__.py +++ b/python/BioSimSpace/__init__.py @@ -257,11 +257,3 @@ def _isVerbose(): __version__ = get_versions()["version"] del get_versions - -import logging as _logging - -for _name, _logger in _logging.root.manager.loggerDict.items(): - _logger.disabled = True -del _logger -del _logging -del _name From c518ffef7f171cb322d3fbf2f0279d4302569722 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 23 Jan 2024 15:34:53 +0000 Subject: [PATCH 004/121] Update copyright range. --- doc/source/conf.py | 2 +- python/BioSimSpace/Align/__init__.py | 2 +- python/BioSimSpace/Align/_align.py | 2 +- python/BioSimSpace/Align/_merge.py | 2 +- python/BioSimSpace/Box/__init__.py | 2 +- python/BioSimSpace/Box/_box.py | 2 +- python/BioSimSpace/Convert/__init__.py | 2 +- python/BioSimSpace/Convert/_convert.py | 2 +- python/BioSimSpace/FreeEnergy/__init__.py | 2 +- python/BioSimSpace/FreeEnergy/_relative.py | 2 +- python/BioSimSpace/FreeEnergy/_utils.py | 2 +- python/BioSimSpace/Gateway/__init__.py | 2 +- python/BioSimSpace/Gateway/_node.py | 2 +- python/BioSimSpace/Gateway/_requirements.py | 2 +- python/BioSimSpace/Gateway/_resources.py | 2 +- python/BioSimSpace/IO/__init__.py | 2 +- python/BioSimSpace/IO/_file_cache.py | 2 +- python/BioSimSpace/IO/_io.py | 2 +- python/BioSimSpace/MD/__init__.py | 2 +- python/BioSimSpace/MD/_md.py | 2 +- python/BioSimSpace/MD/_utils.py | 2 +- python/BioSimSpace/Metadynamics/CollectiveVariable/__init__.py | 2 +- .../Metadynamics/CollectiveVariable/_collective_variable.py | 2 +- python/BioSimSpace/Metadynamics/CollectiveVariable/_distance.py | 2 +- python/BioSimSpace/Metadynamics/CollectiveVariable/_funnel.py | 2 +- python/BioSimSpace/Metadynamics/CollectiveVariable/_rmsd.py | 2 +- python/BioSimSpace/Metadynamics/CollectiveVariable/_torsion.py | 2 +- python/BioSimSpace/Metadynamics/CollectiveVariable/_utils.py | 2 +- python/BioSimSpace/Metadynamics/__init__.py | 2 +- python/BioSimSpace/Metadynamics/_bound.py | 2 +- python/BioSimSpace/Metadynamics/_grid.py | 2 +- python/BioSimSpace/Metadynamics/_metadynamics.py | 2 +- python/BioSimSpace/Metadynamics/_restraint.py | 2 +- python/BioSimSpace/Metadynamics/_utils.py | 2 +- python/BioSimSpace/Node/__init__.py | 2 +- python/BioSimSpace/Node/_node.py | 2 +- python/BioSimSpace/Notebook/__init__.py | 2 +- python/BioSimSpace/Notebook/_plot.py | 2 +- python/BioSimSpace/Notebook/_view.py | 2 +- python/BioSimSpace/Parameters/_Protocol/__init__.py | 2 +- python/BioSimSpace/Parameters/_Protocol/_amber.py | 2 +- python/BioSimSpace/Parameters/_Protocol/_openforcefield.py | 2 +- python/BioSimSpace/Parameters/_Protocol/_protocol.py | 2 +- python/BioSimSpace/Parameters/__init__.py | 2 +- python/BioSimSpace/Parameters/_parameters.py | 2 +- python/BioSimSpace/Parameters/_process.py | 2 +- python/BioSimSpace/Parameters/_utils.py | 2 +- python/BioSimSpace/Process/__init__.py | 2 +- python/BioSimSpace/Process/_amber.py | 2 +- python/BioSimSpace/Process/_gromacs.py | 2 +- python/BioSimSpace/Process/_namd.py | 2 +- python/BioSimSpace/Process/_openmm.py | 2 +- python/BioSimSpace/Process/_plumed.py | 2 +- python/BioSimSpace/Process/_process.py | 2 +- python/BioSimSpace/Process/_process_runner.py | 2 +- python/BioSimSpace/Process/_somd.py | 2 +- python/BioSimSpace/Process/_task.py | 2 +- python/BioSimSpace/Process/_utils.py | 2 +- python/BioSimSpace/Protocol/__init__.py | 2 +- python/BioSimSpace/Protocol/_custom.py | 2 +- python/BioSimSpace/Protocol/_equilibration.py | 2 +- python/BioSimSpace/Protocol/_free_energy_equilibration.py | 2 +- python/BioSimSpace/Protocol/_free_energy_minimisation.py | 2 +- python/BioSimSpace/Protocol/_free_energy_mixin.py | 2 +- python/BioSimSpace/Protocol/_free_energy_production.py | 2 +- python/BioSimSpace/Protocol/_metadynamics.py | 2 +- python/BioSimSpace/Protocol/_minimisation.py | 2 +- python/BioSimSpace/Protocol/_position_restraint_mixin.py | 2 +- python/BioSimSpace/Protocol/_production.py | 2 +- python/BioSimSpace/Protocol/_protocol.py | 2 +- python/BioSimSpace/Protocol/_steering.py | 2 +- python/BioSimSpace/Protocol/_utils.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Align/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Align/_align.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Align/_merge.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Box/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Box/_box.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Convert/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Convert/_convert.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/__init__.py | 2 +- .../Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py | 2 +- .../Sandpit/Exscientia/FreeEnergy/_restraint_search.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_utils.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Gateway/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Gateway/_node.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Gateway/_requirements.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Gateway/_resources.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/IO/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/IO/_file_cache.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/IO/_io.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/MD/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/MD/_md.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/MD/_utils.py | 2 +- .../Exscientia/Metadynamics/CollectiveVariable/__init__.py | 2 +- .../Metadynamics/CollectiveVariable/_collective_variable.py | 2 +- .../Exscientia/Metadynamics/CollectiveVariable/_distance.py | 2 +- .../Exscientia/Metadynamics/CollectiveVariable/_funnel.py | 2 +- .../Sandpit/Exscientia/Metadynamics/CollectiveVariable/_rmsd.py | 2 +- .../Exscientia/Metadynamics/CollectiveVariable/_torsion.py | 2 +- .../Exscientia/Metadynamics/CollectiveVariable/_utils.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Metadynamics/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_bound.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_grid.py | 2 +- .../Sandpit/Exscientia/Metadynamics/_metadynamics.py | 2 +- .../BioSimSpace/Sandpit/Exscientia/Metadynamics/_restraint.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_utils.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Node/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Node/_node.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Notebook/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Notebook/_plot.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Notebook/_view.py | 2 +- .../Sandpit/Exscientia/Parameters/_Protocol/__init__.py | 2 +- .../Sandpit/Exscientia/Parameters/_Protocol/_amber.py | 2 +- .../Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py | 2 +- .../Sandpit/Exscientia/Parameters/_Protocol/_protocol.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Parameters/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Parameters/_parameters.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Parameters/_process.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Parameters/_utils.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Process/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Process/_namd.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Process/_openmm.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Process/_plumed.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Process/_process.py | 2 +- .../BioSimSpace/Sandpit/Exscientia/Process/_process_runner.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Process/_task.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Process/_utils.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Protocol/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Protocol/_custom.py | 2 +- .../BioSimSpace/Sandpit/Exscientia/Protocol/_equilibration.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Protocol/_free_energy.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Protocol/_metadynamics.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Protocol/_minimisation.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Protocol/_production.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Protocol/_protocol.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Protocol/_steering.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Protocol/_utils.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Solvent/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Solvent/_solvent.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Stream/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Stream/_stream.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Trajectory/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_area.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_base_units.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_coordinate.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_length.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_time.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_type.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_vector.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Units/Angle/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Units/Area/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Units/Charge/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Units/Energy/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Units/Length/__init__.py | 2 +- .../BioSimSpace/Sandpit/Exscientia/Units/Pressure/__init__.py | 2 +- .../Sandpit/Exscientia/Units/Temperature/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Units/Time/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Units/Volume/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/Units/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/_Exceptions/__init__.py | 2 +- .../BioSimSpace/Sandpit/Exscientia/_Exceptions/_exceptions.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_atom.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_bond.py | 2 +- .../BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py | 2 +- .../BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecules.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_residue.py | 2 +- .../Sandpit/Exscientia/_SireWrappers/_search_result.py | 2 +- .../Sandpit/Exscientia/_SireWrappers/_sire_wrapper.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_utils.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/_Utils/__init__.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/_Utils/_command_split.py | 2 +- .../BioSimSpace/Sandpit/Exscientia/_Utils/_contextmanagers.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/_Utils/_module_stub.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/_Utils/_workdir.py | 2 +- python/BioSimSpace/Sandpit/Exscientia/__init__.py | 2 +- python/BioSimSpace/Sandpit/__init__.py | 2 +- python/BioSimSpace/Solvent/__init__.py | 2 +- python/BioSimSpace/Solvent/_solvent.py | 2 +- python/BioSimSpace/Stream/__init__.py | 2 +- python/BioSimSpace/Stream/_stream.py | 2 +- python/BioSimSpace/Trajectory/__init__.py | 2 +- python/BioSimSpace/Trajectory/_trajectory.py | 2 +- python/BioSimSpace/Types/__init__.py | 2 +- python/BioSimSpace/Types/_angle.py | 2 +- python/BioSimSpace/Types/_area.py | 2 +- python/BioSimSpace/Types/_base_units.py | 2 +- python/BioSimSpace/Types/_charge.py | 2 +- python/BioSimSpace/Types/_coordinate.py | 2 +- python/BioSimSpace/Types/_energy.py | 2 +- python/BioSimSpace/Types/_general_unit.py | 2 +- python/BioSimSpace/Types/_length.py | 2 +- python/BioSimSpace/Types/_pressure.py | 2 +- python/BioSimSpace/Types/_temperature.py | 2 +- python/BioSimSpace/Types/_time.py | 2 +- python/BioSimSpace/Types/_type.py | 2 +- python/BioSimSpace/Types/_vector.py | 2 +- python/BioSimSpace/Types/_volume.py | 2 +- python/BioSimSpace/Units/Angle/__init__.py | 2 +- python/BioSimSpace/Units/Area/__init__.py | 2 +- python/BioSimSpace/Units/Charge/__init__.py | 2 +- python/BioSimSpace/Units/Energy/__init__.py | 2 +- python/BioSimSpace/Units/Length/__init__.py | 2 +- python/BioSimSpace/Units/Pressure/__init__.py | 2 +- python/BioSimSpace/Units/Temperature/__init__.py | 2 +- python/BioSimSpace/Units/Time/__init__.py | 2 +- python/BioSimSpace/Units/Volume/__init__.py | 2 +- python/BioSimSpace/Units/__init__.py | 2 +- python/BioSimSpace/_Config/__init__.py | 2 +- python/BioSimSpace/_Config/_amber.py | 2 +- python/BioSimSpace/_Config/_config.py | 2 +- python/BioSimSpace/_Config/_gromacs.py | 2 +- python/BioSimSpace/_Config/_somd.py | 2 +- python/BioSimSpace/_Exceptions/__init__.py | 2 +- python/BioSimSpace/_Exceptions/_exceptions.py | 2 +- python/BioSimSpace/_SireWrappers/__init__.py | 2 +- python/BioSimSpace/_SireWrappers/_atom.py | 2 +- python/BioSimSpace/_SireWrappers/_bond.py | 2 +- python/BioSimSpace/_SireWrappers/_molecule.py | 2 +- python/BioSimSpace/_SireWrappers/_molecules.py | 2 +- python/BioSimSpace/_SireWrappers/_residue.py | 2 +- python/BioSimSpace/_SireWrappers/_search_result.py | 2 +- python/BioSimSpace/_SireWrappers/_sire_wrapper.py | 2 +- python/BioSimSpace/_SireWrappers/_system.py | 2 +- python/BioSimSpace/_SireWrappers/_utils.py | 2 +- python/BioSimSpace/_Utils/__init__.py | 2 +- python/BioSimSpace/_Utils/_command_split.py | 2 +- python/BioSimSpace/_Utils/_contextmanagers.py | 2 +- python/BioSimSpace/_Utils/_module_stub.py | 2 +- python/BioSimSpace/_Utils/_workdir.py | 2 +- python/BioSimSpace/__init__.py | 2 +- 245 files changed, 245 insertions(+), 245 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index eb740927a..b69ac7a27 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -43,7 +43,7 @@ # General information about the project. project = "biosimspace" -copyright = "2017-2023" +copyright = "2017-2024" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/python/BioSimSpace/Align/__init__.py b/python/BioSimSpace/Align/__init__.py index c251a1843..47f97664c 100644 --- a/python/BioSimSpace/Align/__init__.py +++ b/python/BioSimSpace/Align/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Align/_align.py b/python/BioSimSpace/Align/_align.py index 4713693c9..bf4551787 100644 --- a/python/BioSimSpace/Align/_align.py +++ b/python/BioSimSpace/Align/_align.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Align/_merge.py b/python/BioSimSpace/Align/_merge.py index 1f843575e..9c2b96309 100644 --- a/python/BioSimSpace/Align/_merge.py +++ b/python/BioSimSpace/Align/_merge.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Box/__init__.py b/python/BioSimSpace/Box/__init__.py index d886d4b57..973cb08ec 100644 --- a/python/BioSimSpace/Box/__init__.py +++ b/python/BioSimSpace/Box/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Box/_box.py b/python/BioSimSpace/Box/_box.py index 4f9085040..a9c163fad 100644 --- a/python/BioSimSpace/Box/_box.py +++ b/python/BioSimSpace/Box/_box.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Convert/__init__.py b/python/BioSimSpace/Convert/__init__.py index 8c44bf95b..919db240a 100644 --- a/python/BioSimSpace/Convert/__init__.py +++ b/python/BioSimSpace/Convert/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Convert/_convert.py b/python/BioSimSpace/Convert/_convert.py index cf6030618..ecb7cb4ea 100644 --- a/python/BioSimSpace/Convert/_convert.py +++ b/python/BioSimSpace/Convert/_convert.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/FreeEnergy/__init__.py b/python/BioSimSpace/FreeEnergy/__init__.py index 7cc1ba2c5..7021e19d8 100644 --- a/python/BioSimSpace/FreeEnergy/__init__.py +++ b/python/BioSimSpace/FreeEnergy/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/FreeEnergy/_relative.py b/python/BioSimSpace/FreeEnergy/_relative.py index 2b0f11096..7c09f5169 100644 --- a/python/BioSimSpace/FreeEnergy/_relative.py +++ b/python/BioSimSpace/FreeEnergy/_relative.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/FreeEnergy/_utils.py b/python/BioSimSpace/FreeEnergy/_utils.py index ec79e3c45..c4c3a61b7 100644 --- a/python/BioSimSpace/FreeEnergy/_utils.py +++ b/python/BioSimSpace/FreeEnergy/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Gateway/__init__.py b/python/BioSimSpace/Gateway/__init__.py index d85a0e8de..3a3a56596 100644 --- a/python/BioSimSpace/Gateway/__init__.py +++ b/python/BioSimSpace/Gateway/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Gateway/_node.py b/python/BioSimSpace/Gateway/_node.py index e71bc80ec..3ec33ebf9 100644 --- a/python/BioSimSpace/Gateway/_node.py +++ b/python/BioSimSpace/Gateway/_node.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Gateway/_requirements.py b/python/BioSimSpace/Gateway/_requirements.py index dd84139d1..c60ca892f 100644 --- a/python/BioSimSpace/Gateway/_requirements.py +++ b/python/BioSimSpace/Gateway/_requirements.py @@ -1,7 +1,7 @@ ##################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Gateway/_resources.py b/python/BioSimSpace/Gateway/_resources.py index c9e47a555..c1c3ce323 100644 --- a/python/BioSimSpace/Gateway/_resources.py +++ b/python/BioSimSpace/Gateway/_resources.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/IO/__init__.py b/python/BioSimSpace/IO/__init__.py index ba81cf48b..405005f35 100644 --- a/python/BioSimSpace/IO/__init__.py +++ b/python/BioSimSpace/IO/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/IO/_file_cache.py b/python/BioSimSpace/IO/_file_cache.py index 175ec6a51..0b55aef9a 100644 --- a/python/BioSimSpace/IO/_file_cache.py +++ b/python/BioSimSpace/IO/_file_cache.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/IO/_io.py b/python/BioSimSpace/IO/_io.py index 705e8f9d6..2f0ddb417 100644 --- a/python/BioSimSpace/IO/_io.py +++ b/python/BioSimSpace/IO/_io.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/MD/__init__.py b/python/BioSimSpace/MD/__init__.py index b9118a23d..629dac75a 100644 --- a/python/BioSimSpace/MD/__init__.py +++ b/python/BioSimSpace/MD/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/MD/_md.py b/python/BioSimSpace/MD/_md.py index 744190ed8..cc0a9d6f8 100644 --- a/python/BioSimSpace/MD/_md.py +++ b/python/BioSimSpace/MD/_md.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/MD/_utils.py b/python/BioSimSpace/MD/_utils.py index d3f923739..dc6b15001 100644 --- a/python/BioSimSpace/MD/_utils.py +++ b/python/BioSimSpace/MD/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/CollectiveVariable/__init__.py b/python/BioSimSpace/Metadynamics/CollectiveVariable/__init__.py index 97642e2ec..a08df8961 100644 --- a/python/BioSimSpace/Metadynamics/CollectiveVariable/__init__.py +++ b/python/BioSimSpace/Metadynamics/CollectiveVariable/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/CollectiveVariable/_collective_variable.py b/python/BioSimSpace/Metadynamics/CollectiveVariable/_collective_variable.py index 9f8670cb9..1a823ff86 100644 --- a/python/BioSimSpace/Metadynamics/CollectiveVariable/_collective_variable.py +++ b/python/BioSimSpace/Metadynamics/CollectiveVariable/_collective_variable.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/CollectiveVariable/_distance.py b/python/BioSimSpace/Metadynamics/CollectiveVariable/_distance.py index f71537497..18eef7c08 100644 --- a/python/BioSimSpace/Metadynamics/CollectiveVariable/_distance.py +++ b/python/BioSimSpace/Metadynamics/CollectiveVariable/_distance.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/CollectiveVariable/_funnel.py b/python/BioSimSpace/Metadynamics/CollectiveVariable/_funnel.py index b0f4c019e..7c8e0ff01 100644 --- a/python/BioSimSpace/Metadynamics/CollectiveVariable/_funnel.py +++ b/python/BioSimSpace/Metadynamics/CollectiveVariable/_funnel.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/CollectiveVariable/_rmsd.py b/python/BioSimSpace/Metadynamics/CollectiveVariable/_rmsd.py index 103fee246..23d7fa0dc 100644 --- a/python/BioSimSpace/Metadynamics/CollectiveVariable/_rmsd.py +++ b/python/BioSimSpace/Metadynamics/CollectiveVariable/_rmsd.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/CollectiveVariable/_torsion.py b/python/BioSimSpace/Metadynamics/CollectiveVariable/_torsion.py index 0eb45bc4d..a4ae82a3b 100644 --- a/python/BioSimSpace/Metadynamics/CollectiveVariable/_torsion.py +++ b/python/BioSimSpace/Metadynamics/CollectiveVariable/_torsion.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/CollectiveVariable/_utils.py b/python/BioSimSpace/Metadynamics/CollectiveVariable/_utils.py index 8d1f1376f..c97de7742 100644 --- a/python/BioSimSpace/Metadynamics/CollectiveVariable/_utils.py +++ b/python/BioSimSpace/Metadynamics/CollectiveVariable/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/__init__.py b/python/BioSimSpace/Metadynamics/__init__.py index 2ed29c4cb..758603aea 100644 --- a/python/BioSimSpace/Metadynamics/__init__.py +++ b/python/BioSimSpace/Metadynamics/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/_bound.py b/python/BioSimSpace/Metadynamics/_bound.py index 1fe275344..f9d33d94f 100644 --- a/python/BioSimSpace/Metadynamics/_bound.py +++ b/python/BioSimSpace/Metadynamics/_bound.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/_grid.py b/python/BioSimSpace/Metadynamics/_grid.py index 69729b877..3668e36ed 100644 --- a/python/BioSimSpace/Metadynamics/_grid.py +++ b/python/BioSimSpace/Metadynamics/_grid.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/_metadynamics.py b/python/BioSimSpace/Metadynamics/_metadynamics.py index d00cd99c4..9c890fba7 100644 --- a/python/BioSimSpace/Metadynamics/_metadynamics.py +++ b/python/BioSimSpace/Metadynamics/_metadynamics.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/_restraint.py b/python/BioSimSpace/Metadynamics/_restraint.py index 192223163..87d6fe855 100644 --- a/python/BioSimSpace/Metadynamics/_restraint.py +++ b/python/BioSimSpace/Metadynamics/_restraint.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Metadynamics/_utils.py b/python/BioSimSpace/Metadynamics/_utils.py index 91cc60aee..f7500952f 100644 --- a/python/BioSimSpace/Metadynamics/_utils.py +++ b/python/BioSimSpace/Metadynamics/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Node/__init__.py b/python/BioSimSpace/Node/__init__.py index d94fd6f0c..c71ad5b6b 100644 --- a/python/BioSimSpace/Node/__init__.py +++ b/python/BioSimSpace/Node/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Node/_node.py b/python/BioSimSpace/Node/_node.py index d2c4b5c17..1536a88d9 100644 --- a/python/BioSimSpace/Node/_node.py +++ b/python/BioSimSpace/Node/_node.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Notebook/__init__.py b/python/BioSimSpace/Notebook/__init__.py index 411d30f04..2ea5ad0f8 100644 --- a/python/BioSimSpace/Notebook/__init__.py +++ b/python/BioSimSpace/Notebook/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Notebook/_plot.py b/python/BioSimSpace/Notebook/_plot.py index 7d6293f8e..7c9e0b3e7 100644 --- a/python/BioSimSpace/Notebook/_plot.py +++ b/python/BioSimSpace/Notebook/_plot.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Notebook/_view.py b/python/BioSimSpace/Notebook/_view.py index 219a3fcea..b594bbb27 100644 --- a/python/BioSimSpace/Notebook/_view.py +++ b/python/BioSimSpace/Notebook/_view.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Parameters/_Protocol/__init__.py b/python/BioSimSpace/Parameters/_Protocol/__init__.py index 1ae7f4cc2..4c3eb9c21 100644 --- a/python/BioSimSpace/Parameters/_Protocol/__init__.py +++ b/python/BioSimSpace/Parameters/_Protocol/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Parameters/_Protocol/_amber.py b/python/BioSimSpace/Parameters/_Protocol/_amber.py index 0b05329a2..69816c1e4 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_amber.py +++ b/python/BioSimSpace/Parameters/_Protocol/_amber.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py index a30b7ab3e..9571e79ce 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Parameters/_Protocol/_protocol.py b/python/BioSimSpace/Parameters/_Protocol/_protocol.py index e1e3fc1c3..266c36683 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_protocol.py +++ b/python/BioSimSpace/Parameters/_Protocol/_protocol.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Parameters/__init__.py b/python/BioSimSpace/Parameters/__init__.py index de89a1d5a..a1463b2d4 100644 --- a/python/BioSimSpace/Parameters/__init__.py +++ b/python/BioSimSpace/Parameters/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Parameters/_parameters.py b/python/BioSimSpace/Parameters/_parameters.py index 945319d99..07a49d24b 100644 --- a/python/BioSimSpace/Parameters/_parameters.py +++ b/python/BioSimSpace/Parameters/_parameters.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Parameters/_process.py b/python/BioSimSpace/Parameters/_process.py index 74e4eabcf..acff2a1e2 100644 --- a/python/BioSimSpace/Parameters/_process.py +++ b/python/BioSimSpace/Parameters/_process.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Parameters/_utils.py b/python/BioSimSpace/Parameters/_utils.py index 5acb501a9..e6b2c89ee 100644 --- a/python/BioSimSpace/Parameters/_utils.py +++ b/python/BioSimSpace/Parameters/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Process/__init__.py b/python/BioSimSpace/Process/__init__.py index 43ac54b14..4066f7352 100644 --- a/python/BioSimSpace/Process/__init__.py +++ b/python/BioSimSpace/Process/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index ec22a4a6e..fd8123fa0 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index c70c9ccc0..87b6bbdba 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Process/_namd.py b/python/BioSimSpace/Process/_namd.py index febef4c1e..25ecaa5f3 100644 --- a/python/BioSimSpace/Process/_namd.py +++ b/python/BioSimSpace/Process/_namd.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Process/_openmm.py b/python/BioSimSpace/Process/_openmm.py index 92a40be04..557bef80e 100644 --- a/python/BioSimSpace/Process/_openmm.py +++ b/python/BioSimSpace/Process/_openmm.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Process/_plumed.py b/python/BioSimSpace/Process/_plumed.py index 9f3dfd52e..aad824087 100644 --- a/python/BioSimSpace/Process/_plumed.py +++ b/python/BioSimSpace/Process/_plumed.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Process/_process.py b/python/BioSimSpace/Process/_process.py index b8e8cd32a..255dee9df 100644 --- a/python/BioSimSpace/Process/_process.py +++ b/python/BioSimSpace/Process/_process.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Process/_process_runner.py b/python/BioSimSpace/Process/_process_runner.py index 3dbe97149..5c45c4f04 100644 --- a/python/BioSimSpace/Process/_process_runner.py +++ b/python/BioSimSpace/Process/_process_runner.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Process/_somd.py b/python/BioSimSpace/Process/_somd.py index c7f8ee5ff..885afa398 100644 --- a/python/BioSimSpace/Process/_somd.py +++ b/python/BioSimSpace/Process/_somd.py @@ -1,7 +1,7 @@ ##################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Process/_task.py b/python/BioSimSpace/Process/_task.py index 42e4fc8d3..7b164a503 100644 --- a/python/BioSimSpace/Process/_task.py +++ b/python/BioSimSpace/Process/_task.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Process/_utils.py b/python/BioSimSpace/Process/_utils.py index 560b592fd..5bc677c3d 100644 --- a/python/BioSimSpace/Process/_utils.py +++ b/python/BioSimSpace/Process/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/__init__.py b/python/BioSimSpace/Protocol/__init__.py index 9f95d4224..aa4a63322 100644 --- a/python/BioSimSpace/Protocol/__init__.py +++ b/python/BioSimSpace/Protocol/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_custom.py b/python/BioSimSpace/Protocol/_custom.py index 55ff5b826..a40a2614c 100644 --- a/python/BioSimSpace/Protocol/_custom.py +++ b/python/BioSimSpace/Protocol/_custom.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_equilibration.py b/python/BioSimSpace/Protocol/_equilibration.py index c28a13ba9..5bed6f73b 100644 --- a/python/BioSimSpace/Protocol/_equilibration.py +++ b/python/BioSimSpace/Protocol/_equilibration.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_free_energy_equilibration.py b/python/BioSimSpace/Protocol/_free_energy_equilibration.py index 3e107dc46..1ab69e2e2 100644 --- a/python/BioSimSpace/Protocol/_free_energy_equilibration.py +++ b/python/BioSimSpace/Protocol/_free_energy_equilibration.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_free_energy_minimisation.py b/python/BioSimSpace/Protocol/_free_energy_minimisation.py index f1d747611..bc50b811c 100644 --- a/python/BioSimSpace/Protocol/_free_energy_minimisation.py +++ b/python/BioSimSpace/Protocol/_free_energy_minimisation.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_free_energy_mixin.py b/python/BioSimSpace/Protocol/_free_energy_mixin.py index b3d3d94e9..8a0b2da2c 100644 --- a/python/BioSimSpace/Protocol/_free_energy_mixin.py +++ b/python/BioSimSpace/Protocol/_free_energy_mixin.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_free_energy_production.py b/python/BioSimSpace/Protocol/_free_energy_production.py index 3210c5c63..0239d0cbc 100644 --- a/python/BioSimSpace/Protocol/_free_energy_production.py +++ b/python/BioSimSpace/Protocol/_free_energy_production.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_metadynamics.py b/python/BioSimSpace/Protocol/_metadynamics.py index 460c4e887..99b49320e 100644 --- a/python/BioSimSpace/Protocol/_metadynamics.py +++ b/python/BioSimSpace/Protocol/_metadynamics.py @@ -1,7 +1,7 @@ ##################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_minimisation.py b/python/BioSimSpace/Protocol/_minimisation.py index ede8de1e9..4daf99fd8 100644 --- a/python/BioSimSpace/Protocol/_minimisation.py +++ b/python/BioSimSpace/Protocol/_minimisation.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_position_restraint_mixin.py b/python/BioSimSpace/Protocol/_position_restraint_mixin.py index 346f7f6e3..b1fda37d8 100644 --- a/python/BioSimSpace/Protocol/_position_restraint_mixin.py +++ b/python/BioSimSpace/Protocol/_position_restraint_mixin.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_production.py b/python/BioSimSpace/Protocol/_production.py index c52153dac..481bfc1e4 100644 --- a/python/BioSimSpace/Protocol/_production.py +++ b/python/BioSimSpace/Protocol/_production.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_protocol.py b/python/BioSimSpace/Protocol/_protocol.py index b4adade75..9d37e004f 100644 --- a/python/BioSimSpace/Protocol/_protocol.py +++ b/python/BioSimSpace/Protocol/_protocol.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_steering.py b/python/BioSimSpace/Protocol/_steering.py index 46563d47a..59e46c511 100644 --- a/python/BioSimSpace/Protocol/_steering.py +++ b/python/BioSimSpace/Protocol/_steering.py @@ -1,7 +1,7 @@ ##################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Protocol/_utils.py b/python/BioSimSpace/Protocol/_utils.py index ee1505d01..eb38e8af0 100644 --- a/python/BioSimSpace/Protocol/_utils.py +++ b/python/BioSimSpace/Protocol/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Align/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Align/__init__.py index 407fd4a89..64e0b2852 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Align/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Align/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Align/_align.py b/python/BioSimSpace/Sandpit/Exscientia/Align/_align.py index 01b1001e9..27731e95d 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Align/_align.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Align/_align.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Align/_merge.py b/python/BioSimSpace/Sandpit/Exscientia/Align/_merge.py index 211109e8f..859662a16 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Align/_merge.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Align/_merge.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Box/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Box/__init__.py index d886d4b57..973cb08ec 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Box/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Box/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Box/_box.py b/python/BioSimSpace/Sandpit/Exscientia/Box/_box.py index 4f9085040..a9c163fad 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Box/_box.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Box/_box.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Convert/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Convert/__init__.py index 8c44bf95b..919db240a 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Convert/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Convert/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Convert/_convert.py b/python/BioSimSpace/Sandpit/Exscientia/Convert/_convert.py index cf6030618..ecb7cb4ea 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Convert/_convert.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Convert/_convert.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/__init__.py index 7034e087b..2d7ce605b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py index 95692bdc5..0173acab4 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py index 499846255..0cd0b3d97 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py index 30c4b0af4..cb501bbb8 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_utils.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_utils.py index 222c17c9b..540be2bf5 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_utils.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Gateway/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Gateway/__init__.py index d85a0e8de..3a3a56596 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Gateway/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Gateway/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Gateway/_node.py b/python/BioSimSpace/Sandpit/Exscientia/Gateway/_node.py index e71bc80ec..3ec33ebf9 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Gateway/_node.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Gateway/_node.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Gateway/_requirements.py b/python/BioSimSpace/Sandpit/Exscientia/Gateway/_requirements.py index d4d3ed12e..24626ed24 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Gateway/_requirements.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Gateway/_requirements.py @@ -1,7 +1,7 @@ ##################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Gateway/_resources.py b/python/BioSimSpace/Sandpit/Exscientia/Gateway/_resources.py index c9e47a555..c1c3ce323 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Gateway/_resources.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Gateway/_resources.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/IO/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/IO/__init__.py index fd390278f..e83faacca 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/IO/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/IO/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/IO/_file_cache.py b/python/BioSimSpace/Sandpit/Exscientia/IO/_file_cache.py index 175ec6a51..0b55aef9a 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/IO/_file_cache.py +++ b/python/BioSimSpace/Sandpit/Exscientia/IO/_file_cache.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py index 705e8f9d6..2f0ddb417 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py +++ b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/MD/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/MD/__init__.py index b9118a23d..629dac75a 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/MD/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/MD/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/MD/_md.py b/python/BioSimSpace/Sandpit/Exscientia/MD/_md.py index 3b6e32a8c..6d1280268 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/MD/_md.py +++ b/python/BioSimSpace/Sandpit/Exscientia/MD/_md.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/MD/_utils.py b/python/BioSimSpace/Sandpit/Exscientia/MD/_utils.py index e6f9dbec6..fcd25e371 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/MD/_utils.py +++ b/python/BioSimSpace/Sandpit/Exscientia/MD/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/__init__.py index 97642e2ec..a08df8961 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_collective_variable.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_collective_variable.py index 9f8670cb9..1a823ff86 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_collective_variable.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_collective_variable.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_distance.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_distance.py index f71537497..18eef7c08 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_distance.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_distance.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_funnel.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_funnel.py index b0f4c019e..7c8e0ff01 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_funnel.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_funnel.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_rmsd.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_rmsd.py index 103fee246..23d7fa0dc 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_rmsd.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_rmsd.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_torsion.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_torsion.py index 0eb45bc4d..a4ae82a3b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_torsion.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_torsion.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_utils.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_utils.py index 8d1f1376f..c97de7742 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_utils.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/__init__.py index 2ed29c4cb..758603aea 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_bound.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_bound.py index 1fe275344..f9d33d94f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_bound.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_bound.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_grid.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_grid.py index 69729b877..3668e36ed 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_grid.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_grid.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_metadynamics.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_metadynamics.py index d00cd99c4..9c890fba7 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_metadynamics.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_metadynamics.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_restraint.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_restraint.py index 192223163..87d6fe855 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_restraint.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_restraint.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_utils.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_utils.py index c92caffb1..5c2296291 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_utils.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Node/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Node/__init__.py index d94fd6f0c..c71ad5b6b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Node/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Node/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Node/_node.py b/python/BioSimSpace/Sandpit/Exscientia/Node/_node.py index d362fe8d7..8faed8a51 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Node/_node.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Node/_node.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Notebook/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Notebook/__init__.py index 411d30f04..2ea5ad0f8 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Notebook/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Notebook/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Notebook/_plot.py b/python/BioSimSpace/Sandpit/Exscientia/Notebook/_plot.py index 7d6293f8e..7c9e0b3e7 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Notebook/_plot.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Notebook/_plot.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Notebook/_view.py b/python/BioSimSpace/Sandpit/Exscientia/Notebook/_view.py index 219a3fcea..b594bbb27 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Notebook/_view.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Notebook/_view.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/__init__.py index 1ae7f4cc2..4c3eb9c21 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py index 0b05329a2..69816c1e4 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py index a30b7ab3e..9571e79ce 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_protocol.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_protocol.py index e1e3fc1c3..266c36683 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_protocol.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_protocol.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/__init__.py index de89a1d5a..a1463b2d4 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_parameters.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_parameters.py index 945319d99..07a49d24b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_parameters.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_parameters.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_process.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_process.py index 74e4eabcf..acff2a1e2 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_process.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_process.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_utils.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_utils.py index 5acb501a9..e6b2c89ee 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_utils.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Process/__init__.py index 43ac54b14..4066f7352 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py index d84e7eaa5..7c9f66309 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py index 93c86f6d4..097f3074f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_namd.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_namd.py index eb60dce1a..314f31a92 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_namd.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_namd.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_openmm.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_openmm.py index 07dc8c7a2..ef4486d1f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_openmm.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_openmm.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_plumed.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_plumed.py index 9f3dfd52e..aad824087 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_plumed.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_plumed.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_process.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_process.py index 58022f599..1228a11ed 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_process.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_process.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_process_runner.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_process_runner.py index 3dbe97149..5c45c4f04 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_process_runner.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_process_runner.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py index 8db9fc8be..bc5e13c74 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_task.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_task.py index 42e4fc8d3..7b164a503 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_task.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_task.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_utils.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_utils.py index 560b592fd..5bc677c3d 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_utils.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/__init__.py index 3d3e9bd20..e4051a340 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_custom.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_custom.py index 3a4bea240..a19ee676c 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_custom.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_custom.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_equilibration.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_equilibration.py index 225052171..fa60473d5 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_equilibration.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_equilibration.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_free_energy.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_free_energy.py index 2ed1caa74..b7dcde852 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_free_energy.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_free_energy.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_metadynamics.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_metadynamics.py index 460c4e887..99b49320e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_metadynamics.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_metadynamics.py @@ -1,7 +1,7 @@ ##################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_minimisation.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_minimisation.py index dc6f3b0c6..49a110a70 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_minimisation.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_minimisation.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_production.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_production.py index 45249c693..9604d7fb8 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_production.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_production.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_protocol.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_protocol.py index 49c0f6937..207779f48 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_protocol.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_protocol.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_steering.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_steering.py index 17081126c..4dd732449 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_steering.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_steering.py @@ -1,7 +1,7 @@ ##################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_utils.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_utils.py index e73802f53..e42c685b4 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_utils.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Solvent/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Solvent/__init__.py index acca19b68..5d7c02cd6 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Solvent/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Solvent/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Solvent/_solvent.py b/python/BioSimSpace/Sandpit/Exscientia/Solvent/_solvent.py index 0bcb4a682..98cc77019 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Solvent/_solvent.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Solvent/_solvent.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Stream/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Stream/__init__.py index 72ee72f07..703eb7ba2 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Stream/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Stream/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Stream/_stream.py b/python/BioSimSpace/Sandpit/Exscientia/Stream/_stream.py index ae14ea6b6..7af2cbe53 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Stream/_stream.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Stream/_stream.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Trajectory/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Trajectory/__init__.py index 3f66d83d8..1a80928df 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Trajectory/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Trajectory/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py b/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py index 2332e74f6..7b5f9d45e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Types/__init__.py index 6e05826c7..2070e5065 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py index 759a71724..f1fc753fa 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py index ec8484cca..9e1eb9464 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_base_units.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_base_units.py index 240607564..931668f84 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_base_units.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_base_units.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py index 7717f481e..0c9879ed1 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_coordinate.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_coordinate.py index 0cddb2c46..261e1a1ad 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_coordinate.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_coordinate.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py index bb293a17a..fa775fb26 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py index deca60800..256b9bd8b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py index 5eb10fb07..2096b3d20 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py index 699d9d5f2..672dc2642 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py index d11d70f0c..8f0a7cb70 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py index 19fc10401..0d1603205 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py index f01635631..a0de475e3 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_vector.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_vector.py index f961b367e..63eb35294 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_vector.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_vector.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py index 4b255e01a..e664557da 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Units/Angle/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Units/Angle/__init__.py index 6e5b2e424..a18a1603f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Units/Angle/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Units/Angle/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Units/Area/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Units/Area/__init__.py index 92760fea4..f0b8dc704 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Units/Area/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Units/Area/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Units/Charge/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Units/Charge/__init__.py index 6fc63bfa6..14de332dd 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Units/Charge/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Units/Charge/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Units/Energy/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Units/Energy/__init__.py index 9b9b80c28..15fa4e429 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Units/Energy/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Units/Energy/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Units/Length/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Units/Length/__init__.py index 33e91655a..eede0da7a 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Units/Length/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Units/Length/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Units/Pressure/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Units/Pressure/__init__.py index c53f165f9..76a9b8ac0 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Units/Pressure/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Units/Pressure/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Units/Temperature/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Units/Temperature/__init__.py index 0374d48f2..dd0c8b8c0 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Units/Temperature/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Units/Temperature/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Units/Time/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Units/Time/__init__.py index 60606fc9f..998de96ed 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Units/Time/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Units/Time/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Units/Volume/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Units/Volume/__init__.py index c86bba991..cc54e18ed 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Units/Volume/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Units/Volume/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/Units/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/Units/__init__.py index 8ad224ad4..227d58466 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Units/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Units/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_Exceptions/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/_Exceptions/__init__.py index 882b9d40b..82fcce257 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_Exceptions/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_Exceptions/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_Exceptions/_exceptions.py b/python/BioSimSpace/Sandpit/Exscientia/_Exceptions/_exceptions.py index d19a0d1b8..577131c29 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_Exceptions/_exceptions.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_Exceptions/_exceptions.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/__init__.py index ee673968e..739ad8e26 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_atom.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_atom.py index 2fb397a3b..92b3142f0 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_atom.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_atom.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_bond.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_bond.py index eef0b2f5b..50bec000b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_bond.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_bond.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py index 60f43224d..b1449fa4e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecules.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecules.py index dd1845949..838d526eb 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecules.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecules.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_residue.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_residue.py index fbf3705d3..27356b00d 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_residue.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_residue.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_search_result.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_search_result.py index 0ce4b7062..4c039f855 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_search_result.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_search_result.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_sire_wrapper.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_sire_wrapper.py index 2e4e41152..7cb3a7cc4 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_sire_wrapper.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_sire_wrapper.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py index 853038840..e51f141c7 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_utils.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_utils.py index faab3e069..6d735cc65 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_utils.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_Utils/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/_Utils/__init__.py index 0c845d8be..88ed9a85f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_Utils/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_Utils/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_Utils/_command_split.py b/python/BioSimSpace/Sandpit/Exscientia/_Utils/_command_split.py index 5ee458c50..60ab56adf 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_Utils/_command_split.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_Utils/_command_split.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Christopher Woods # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_Utils/_contextmanagers.py b/python/BioSimSpace/Sandpit/Exscientia/_Utils/_contextmanagers.py index 87d459099..0de140173 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_Utils/_contextmanagers.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_Utils/_contextmanagers.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_Utils/_module_stub.py b/python/BioSimSpace/Sandpit/Exscientia/_Utils/_module_stub.py index 5cb1aa5a8..9b65afc78 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_Utils/_module_stub.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_Utils/_module_stub.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/_Utils/_workdir.py b/python/BioSimSpace/Sandpit/Exscientia/_Utils/_workdir.py index 87b1c4754..1e564e3c8 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_Utils/_workdir.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_Utils/_workdir.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/Exscientia/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/__init__.py index d99b13fb7..badf48913 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Sandpit/__init__.py b/python/BioSimSpace/Sandpit/__init__.py index ba9b86a83..eb2d80122 100644 --- a/python/BioSimSpace/Sandpit/__init__.py +++ b/python/BioSimSpace/Sandpit/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Solvent/__init__.py b/python/BioSimSpace/Solvent/__init__.py index 07f5bcc4d..7354481d8 100644 --- a/python/BioSimSpace/Solvent/__init__.py +++ b/python/BioSimSpace/Solvent/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Solvent/_solvent.py b/python/BioSimSpace/Solvent/_solvent.py index 0bcb4a682..98cc77019 100644 --- a/python/BioSimSpace/Solvent/_solvent.py +++ b/python/BioSimSpace/Solvent/_solvent.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Stream/__init__.py b/python/BioSimSpace/Stream/__init__.py index 72ee72f07..703eb7ba2 100644 --- a/python/BioSimSpace/Stream/__init__.py +++ b/python/BioSimSpace/Stream/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Stream/_stream.py b/python/BioSimSpace/Stream/_stream.py index ae14ea6b6..7af2cbe53 100644 --- a/python/BioSimSpace/Stream/_stream.py +++ b/python/BioSimSpace/Stream/_stream.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Trajectory/__init__.py b/python/BioSimSpace/Trajectory/__init__.py index 3f66d83d8..1a80928df 100644 --- a/python/BioSimSpace/Trajectory/__init__.py +++ b/python/BioSimSpace/Trajectory/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Trajectory/_trajectory.py b/python/BioSimSpace/Trajectory/_trajectory.py index 2332e74f6..7b5f9d45e 100644 --- a/python/BioSimSpace/Trajectory/_trajectory.py +++ b/python/BioSimSpace/Trajectory/_trajectory.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/__init__.py b/python/BioSimSpace/Types/__init__.py index 6e05826c7..2070e5065 100644 --- a/python/BioSimSpace/Types/__init__.py +++ b/python/BioSimSpace/Types/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_angle.py b/python/BioSimSpace/Types/_angle.py index 759a71724..f1fc753fa 100644 --- a/python/BioSimSpace/Types/_angle.py +++ b/python/BioSimSpace/Types/_angle.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_area.py b/python/BioSimSpace/Types/_area.py index ec8484cca..9e1eb9464 100644 --- a/python/BioSimSpace/Types/_area.py +++ b/python/BioSimSpace/Types/_area.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_base_units.py b/python/BioSimSpace/Types/_base_units.py index 240607564..931668f84 100644 --- a/python/BioSimSpace/Types/_base_units.py +++ b/python/BioSimSpace/Types/_base_units.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_charge.py b/python/BioSimSpace/Types/_charge.py index 7717f481e..0c9879ed1 100644 --- a/python/BioSimSpace/Types/_charge.py +++ b/python/BioSimSpace/Types/_charge.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_coordinate.py b/python/BioSimSpace/Types/_coordinate.py index 0cddb2c46..261e1a1ad 100644 --- a/python/BioSimSpace/Types/_coordinate.py +++ b/python/BioSimSpace/Types/_coordinate.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_energy.py b/python/BioSimSpace/Types/_energy.py index bb293a17a..fa775fb26 100644 --- a/python/BioSimSpace/Types/_energy.py +++ b/python/BioSimSpace/Types/_energy.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_general_unit.py b/python/BioSimSpace/Types/_general_unit.py index deca60800..256b9bd8b 100644 --- a/python/BioSimSpace/Types/_general_unit.py +++ b/python/BioSimSpace/Types/_general_unit.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_length.py b/python/BioSimSpace/Types/_length.py index 5eb10fb07..2096b3d20 100644 --- a/python/BioSimSpace/Types/_length.py +++ b/python/BioSimSpace/Types/_length.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_pressure.py b/python/BioSimSpace/Types/_pressure.py index 699d9d5f2..672dc2642 100644 --- a/python/BioSimSpace/Types/_pressure.py +++ b/python/BioSimSpace/Types/_pressure.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_temperature.py b/python/BioSimSpace/Types/_temperature.py index f97d2b956..fe0693662 100644 --- a/python/BioSimSpace/Types/_temperature.py +++ b/python/BioSimSpace/Types/_temperature.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_time.py b/python/BioSimSpace/Types/_time.py index 19fc10401..0d1603205 100644 --- a/python/BioSimSpace/Types/_time.py +++ b/python/BioSimSpace/Types/_time.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_type.py b/python/BioSimSpace/Types/_type.py index f01635631..a0de475e3 100644 --- a/python/BioSimSpace/Types/_type.py +++ b/python/BioSimSpace/Types/_type.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_vector.py b/python/BioSimSpace/Types/_vector.py index f961b367e..63eb35294 100644 --- a/python/BioSimSpace/Types/_vector.py +++ b/python/BioSimSpace/Types/_vector.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Types/_volume.py b/python/BioSimSpace/Types/_volume.py index 4b255e01a..e664557da 100644 --- a/python/BioSimSpace/Types/_volume.py +++ b/python/BioSimSpace/Types/_volume.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Units/Angle/__init__.py b/python/BioSimSpace/Units/Angle/__init__.py index 6e5b2e424..a18a1603f 100644 --- a/python/BioSimSpace/Units/Angle/__init__.py +++ b/python/BioSimSpace/Units/Angle/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Units/Area/__init__.py b/python/BioSimSpace/Units/Area/__init__.py index 92760fea4..f0b8dc704 100644 --- a/python/BioSimSpace/Units/Area/__init__.py +++ b/python/BioSimSpace/Units/Area/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Units/Charge/__init__.py b/python/BioSimSpace/Units/Charge/__init__.py index 6fc63bfa6..14de332dd 100644 --- a/python/BioSimSpace/Units/Charge/__init__.py +++ b/python/BioSimSpace/Units/Charge/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Units/Energy/__init__.py b/python/BioSimSpace/Units/Energy/__init__.py index 9b9b80c28..15fa4e429 100644 --- a/python/BioSimSpace/Units/Energy/__init__.py +++ b/python/BioSimSpace/Units/Energy/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Units/Length/__init__.py b/python/BioSimSpace/Units/Length/__init__.py index 33e91655a..eede0da7a 100644 --- a/python/BioSimSpace/Units/Length/__init__.py +++ b/python/BioSimSpace/Units/Length/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Units/Pressure/__init__.py b/python/BioSimSpace/Units/Pressure/__init__.py index c53f165f9..76a9b8ac0 100644 --- a/python/BioSimSpace/Units/Pressure/__init__.py +++ b/python/BioSimSpace/Units/Pressure/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Units/Temperature/__init__.py b/python/BioSimSpace/Units/Temperature/__init__.py index 0374d48f2..dd0c8b8c0 100644 --- a/python/BioSimSpace/Units/Temperature/__init__.py +++ b/python/BioSimSpace/Units/Temperature/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Units/Time/__init__.py b/python/BioSimSpace/Units/Time/__init__.py index 60606fc9f..998de96ed 100644 --- a/python/BioSimSpace/Units/Time/__init__.py +++ b/python/BioSimSpace/Units/Time/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Units/Volume/__init__.py b/python/BioSimSpace/Units/Volume/__init__.py index c86bba991..cc54e18ed 100644 --- a/python/BioSimSpace/Units/Volume/__init__.py +++ b/python/BioSimSpace/Units/Volume/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/Units/__init__.py b/python/BioSimSpace/Units/__init__.py index 8ad224ad4..227d58466 100644 --- a/python/BioSimSpace/Units/__init__.py +++ b/python/BioSimSpace/Units/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_Config/__init__.py b/python/BioSimSpace/_Config/__init__.py index c19d122ea..a580dbfb3 100644 --- a/python/BioSimSpace/_Config/__init__.py +++ b/python/BioSimSpace/_Config/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index ddd51ed41..a25a5109c 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_Config/_config.py b/python/BioSimSpace/_Config/_config.py index d8b5ce7ee..3aea03484 100644 --- a/python/BioSimSpace/_Config/_config.py +++ b/python/BioSimSpace/_Config/_config.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_Config/_gromacs.py b/python/BioSimSpace/_Config/_gromacs.py index 35d483f3d..23c50e613 100644 --- a/python/BioSimSpace/_Config/_gromacs.py +++ b/python/BioSimSpace/_Config/_gromacs.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_Config/_somd.py b/python/BioSimSpace/_Config/_somd.py index 613798e34..12ce38b7d 100644 --- a/python/BioSimSpace/_Config/_somd.py +++ b/python/BioSimSpace/_Config/_somd.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_Exceptions/__init__.py b/python/BioSimSpace/_Exceptions/__init__.py index 882b9d40b..82fcce257 100644 --- a/python/BioSimSpace/_Exceptions/__init__.py +++ b/python/BioSimSpace/_Exceptions/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_Exceptions/_exceptions.py b/python/BioSimSpace/_Exceptions/_exceptions.py index d19a0d1b8..577131c29 100644 --- a/python/BioSimSpace/_Exceptions/_exceptions.py +++ b/python/BioSimSpace/_Exceptions/_exceptions.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_SireWrappers/__init__.py b/python/BioSimSpace/_SireWrappers/__init__.py index ee673968e..739ad8e26 100644 --- a/python/BioSimSpace/_SireWrappers/__init__.py +++ b/python/BioSimSpace/_SireWrappers/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_SireWrappers/_atom.py b/python/BioSimSpace/_SireWrappers/_atom.py index 2fb397a3b..92b3142f0 100644 --- a/python/BioSimSpace/_SireWrappers/_atom.py +++ b/python/BioSimSpace/_SireWrappers/_atom.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_SireWrappers/_bond.py b/python/BioSimSpace/_SireWrappers/_bond.py index eef0b2f5b..50bec000b 100644 --- a/python/BioSimSpace/_SireWrappers/_bond.py +++ b/python/BioSimSpace/_SireWrappers/_bond.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_SireWrappers/_molecule.py b/python/BioSimSpace/_SireWrappers/_molecule.py index 424d6196d..8138e9b17 100644 --- a/python/BioSimSpace/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/_SireWrappers/_molecule.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_SireWrappers/_molecules.py b/python/BioSimSpace/_SireWrappers/_molecules.py index dd1845949..838d526eb 100644 --- a/python/BioSimSpace/_SireWrappers/_molecules.py +++ b/python/BioSimSpace/_SireWrappers/_molecules.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_SireWrappers/_residue.py b/python/BioSimSpace/_SireWrappers/_residue.py index fbf3705d3..27356b00d 100644 --- a/python/BioSimSpace/_SireWrappers/_residue.py +++ b/python/BioSimSpace/_SireWrappers/_residue.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_SireWrappers/_search_result.py b/python/BioSimSpace/_SireWrappers/_search_result.py index 0ce4b7062..4c039f855 100644 --- a/python/BioSimSpace/_SireWrappers/_search_result.py +++ b/python/BioSimSpace/_SireWrappers/_search_result.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_SireWrappers/_sire_wrapper.py b/python/BioSimSpace/_SireWrappers/_sire_wrapper.py index 2e4e41152..7cb3a7cc4 100644 --- a/python/BioSimSpace/_SireWrappers/_sire_wrapper.py +++ b/python/BioSimSpace/_SireWrappers/_sire_wrapper.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_SireWrappers/_system.py b/python/BioSimSpace/_SireWrappers/_system.py index 3ab8258f9..52ed9c083 100644 --- a/python/BioSimSpace/_SireWrappers/_system.py +++ b/python/BioSimSpace/_SireWrappers/_system.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_SireWrappers/_utils.py b/python/BioSimSpace/_SireWrappers/_utils.py index faab3e069..6d735cc65 100644 --- a/python/BioSimSpace/_SireWrappers/_utils.py +++ b/python/BioSimSpace/_SireWrappers/_utils.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_Utils/__init__.py b/python/BioSimSpace/_Utils/__init__.py index 0c845d8be..88ed9a85f 100644 --- a/python/BioSimSpace/_Utils/__init__.py +++ b/python/BioSimSpace/_Utils/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_Utils/_command_split.py b/python/BioSimSpace/_Utils/_command_split.py index 5ee458c50..60ab56adf 100644 --- a/python/BioSimSpace/_Utils/_command_split.py +++ b/python/BioSimSpace/_Utils/_command_split.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Christopher Woods # diff --git a/python/BioSimSpace/_Utils/_contextmanagers.py b/python/BioSimSpace/_Utils/_contextmanagers.py index 87d459099..0de140173 100644 --- a/python/BioSimSpace/_Utils/_contextmanagers.py +++ b/python/BioSimSpace/_Utils/_contextmanagers.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_Utils/_module_stub.py b/python/BioSimSpace/_Utils/_module_stub.py index 5cb1aa5a8..9b65afc78 100644 --- a/python/BioSimSpace/_Utils/_module_stub.py +++ b/python/BioSimSpace/_Utils/_module_stub.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/_Utils/_workdir.py b/python/BioSimSpace/_Utils/_workdir.py index 87b1c4754..1e564e3c8 100644 --- a/python/BioSimSpace/_Utils/_workdir.py +++ b/python/BioSimSpace/_Utils/_workdir.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # diff --git a/python/BioSimSpace/__init__.py b/python/BioSimSpace/__init__.py index 92fe48f0b..b14f0228f 100644 --- a/python/BioSimSpace/__init__.py +++ b/python/BioSimSpace/__init__.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # From 3d3b1971b5e1e951e82211a5f30739b4b766cddc Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 24 Jan 2024 14:57:52 +0000 Subject: [PATCH 005/121] Match atoms by coordinates. --- .../Sandpit/Exscientia/_SireWrappers/_molecule.py | 13 +++---------- python/BioSimSpace/_SireWrappers/_molecule.py | 13 +++---------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py index 60f43224d..3d919ef3f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py @@ -787,9 +787,8 @@ def makeCompatibleWith( # Have we matched all of the atoms? if len(matches) < num_atoms0: - # Atom names might have changed. Try to match by residue index - # and coordinates. - matcher = _SireMol.ResIdxAtomCoordMatcher() + # Atom names or order might have changed. Try to match by coordinates. + matcher = _SireMol.AtomCoordMatcher() matches = matcher.match(mol0, mol1) # We need to rename the atoms. @@ -989,9 +988,6 @@ def makeCompatibleWith( # Tally counter for the total number of matches. num_matches = 0 - # Initialise the offset. - offset = 0 - # Get the molecule numbers in the system. mol_nums = mol1.molNums() @@ -1001,16 +997,13 @@ def makeCompatibleWith( mol = mol1[num] # Initialise the matcher. - matcher = _SireMol.ResIdxAtomCoordMatcher(_SireMol.ResIdx(offset)) + matcher = _SireMol.AtomCoordMatcher() # Get the matches for this molecule and append to the list. match = matcher.match(mol0, mol) matches.append(match) num_matches += len(match) - # Increment the offset. - offset += mol.nResidues() - # Have we matched all of the atoms? if num_matches < num_atoms0: raise _IncompatibleError("Failed to match all atoms!") diff --git a/python/BioSimSpace/_SireWrappers/_molecule.py b/python/BioSimSpace/_SireWrappers/_molecule.py index 424d6196d..0d03df978 100644 --- a/python/BioSimSpace/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/_SireWrappers/_molecule.py @@ -743,9 +743,8 @@ def makeCompatibleWith( # Have we matched all of the atoms? if len(matches) < num_atoms0: - # Atom names might have changed. Try to match by residue index - # and coordinates. - matcher = _SireMol.ResIdxAtomCoordMatcher() + # Atom names or order might have changed. Try to match by coordinates. + matcher = _SireMol.AtomCoordMatcher() matches = matcher.match(mol0, mol1) # We need to rename the atoms. @@ -945,9 +944,6 @@ def makeCompatibleWith( # Tally counter for the total number of matches. num_matches = 0 - # Initialise the offset. - offset = 0 - # Get the molecule numbers in the system. mol_nums = mol1.molNums() @@ -957,16 +953,13 @@ def makeCompatibleWith( mol = mol1[num] # Initialise the matcher. - matcher = _SireMol.ResIdxAtomCoordMatcher(_SireMol.ResIdx(offset)) + matcher = _SireMol.AtomCoordMatcher() # Get the matches for this molecule and append to the list. match = matcher.match(mol0, mol) matches.append(match) num_matches += len(match) - # Increment the offset. - offset += mol.nResidues() - # Have we matched all of the atoms? if num_matches < num_atoms0: raise _IncompatibleError("Failed to match all atoms!") From 90855ee53c6b9da9db0e0421da5044371c1ecd5b Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 5 Feb 2024 12:21:25 +0000 Subject: [PATCH 006/121] Add _generate_binary_run_file staticmethod. --- python/BioSimSpace/FreeEnergy/_relative.py | 23 +-- python/BioSimSpace/Process/_gromacs.py | 152 ++++++++++++++--- .../FreeEnergy/_alchemical_free_energy.py | 21 +-- .../Sandpit/Exscientia/Process/_gromacs.py | 154 +++++++++++++++--- 4 files changed, 269 insertions(+), 81 deletions(-) diff --git a/python/BioSimSpace/FreeEnergy/_relative.py b/python/BioSimSpace/FreeEnergy/_relative.py index 7c09f5169..44d7375d9 100644 --- a/python/BioSimSpace/FreeEnergy/_relative.py +++ b/python/BioSimSpace/FreeEnergy/_relative.py @@ -2060,9 +2060,7 @@ def _initialise_runner(self, system): extra_lines=self._extra_lines, property_map=self._property_map, ) - if self._setup_only: - del first_process - else: + if not self._setup_only: processes.append(first_process) # Loop over the rest of the lambda values. @@ -2139,30 +2137,19 @@ def _initialise_runner(self, system): f.write(line) mdp = new_dir + "/gromacs.mdp" - mdp_out = new_dir + "/gromacs.out.mdp" gro = new_dir + "/gromacs.gro" top = new_dir + "/gromacs.top" tpr = new_dir + "/gromacs.tpr" # Use grompp to generate the portable binary run input file. - command = "%s grompp -f %s -po %s -c %s -p %s -r %s -o %s" % ( - _gmx_exe, + first_process._generate_binary_run_file( mdp, - mdp_out, gro, top, - gro, tpr, - ) - - # Run the command. If this worked for the first lambda value, - # then it should work for all others. - proc = _subprocess.run( - _Utils.command_split(command), - shell=False, - text=True, - stdout=_subprocess.PIPE, - stderr=_subprocess.PIPE, + first_process._exe, + ignore_warnings=self._ignore_warnings, + show_errors=self._show_errors, ) # Create a copy of the process and update the working diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index 87b6bbdba..3e9bec127 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -421,40 +421,119 @@ def _generate_args(self): if isinstance(self._protocol, (_Protocol.Metadynamics, _Protocol.Steering)): self.setArg("-plumed", "plumed.dat") - def _generate_binary_run_file(self): - """Use grommp to generate the binary run input file.""" + @staticmethod + def _generate_binary_run_file( + mdp_file, + gro_file, + top_file, + tpr_file, + exe, + checkpoint_file=None, + ignore_warnings=False, + show_errors=True, + ): + """ + Use grommp to generate the binary run input file. + + Parameters + ---------- + + mdp_file : str + The path to the input mdp file. + + gro_file : str + The path to the input coordinate file. + + top_file : str + The path to the input topology file. + + tpr_file : str + The path to the output binary run file. + + exe : str + The path to the GROMACS executable. + + checkpoint_file : str + The path to a checkpoint file from a previous run. This can be used + to continue an existing simulation. Currently we only support the + use of checkpoint files for Equilibration protocols. + + ignore_warnings : bool + Whether to ignore warnings when generating the binary run file + with 'gmx grompp'. By default, these warnings are elevated to + errors and will halt the program. + + show_errors : bool + Whether to show warning/error messages when generating the binary + run file. + """ + + if not isinstance(mdp_file, str): + raise ValueError("'mdp_file' must be of type 'str'.") + if not _os.path.isfile(mdp_file): + raise IOError(f"'mdp_file' doesn't exist: '{mdp_file}'") + + if not isinstance(gro_file, str): + raise ValueError("'gro_file' must be of type 'str'.") + if not _os.path.isfile(gro_file): + raise IOError(f"'gro_file' doesn't exist: '{gro_file}'") + + if not isinstance(top_file, str): + raise ValueError("'top_file' must be of type 'str'.") + if not _os.path.isfile(top_file): + raise IOError(f"'top_file' doesn't exist: '{top_file}'") + + if not isinstance(tpr_file, str): + raise ValueError("'tpr_file' must be of type 'str'.") + + if not isinstance(exe, str): + raise ValueError("'exe' must be of type 'str'.") + if not _os.path.isfile(exe): + raise IOError(f"'exe' doesn't exist: '{exe}'") + + if checkpoint_file is not None: + if not isinstance(checkpoint_file, str): + raise ValueError("'checkpoint_file' must be of type 'str'.") + if not _os.path.isfile(checkpoint_file): + raise IOError(f"'checkpoint_file' doesn't exist: '{checkpoint_file}'") + + if not isinstance(ignore_warnings, bool): + raise ValueError("'ignore_warnings' must be of type 'bool'") + + if not isinstance(show_errors, bool): + raise ValueError("'show_errors' must be of type 'bool'") # Create the name of the output mdp file. mdp_out = ( - _os.path.dirname(self._config_file) - + "/%s.out.mdp" % _os.path.basename(self._config_file).split(".")[0] + _os.path.dirname(mdp_file) + + "/%s.out.mdp" % _os.path.basename(mdp_file).split(".")[0] ) # Use grompp to generate the portable binary run input file. - if self._checkpoint_file is not None: + if checkpoint_file is not None: command = "%s grompp -f %s -po %s -c %s -p %s -r %s -t %s -o %s" % ( - self._exe, - self._config_file, + exe, + mdp_file, mdp_out, - self._gro_file, - self._top_file, - self._gro_file, - self._checkpoint_file, - self._tpr_file, + gro_file, + top_file, + gro_file, + checkpoint_file, + tpr_file, ) else: command = "%s grompp -f %s -po %s -c %s -p %s -r %s -o %s" % ( - self._exe, - self._config_file, + exe, + mdp_file, mdp_out, - self._gro_file, - self._top_file, - self._gro_file, - self._tpr_file, + gro_file, + top_file, + gro_file, + tpr_file, ) # Warnings don't trigger an error. - if self._ignore_warnings: + if ignore_warnings: command += " --maxwarn 9999" # Run the command. @@ -469,7 +548,7 @@ def _generate_binary_run_file(self): # Check that grompp ran successfully. if proc.returncode != 0: # Handle errors and warnings. - if self._show_errors: + if show_errors: # Capture errors and warnings from the grompp output. errors = [] warnings = [] @@ -531,14 +610,32 @@ def addToConfig(self, config): super().addToConfig(config) # Use grompp to generate the portable binary run input file. - self._generate_binary_run_file() + self._generate_binary_run_file( + self._config_file, + self._gro_file, + self._top_file, + self._tpr_file, + self._exe, + self._checkpoint_file, + self._ignore_warnings, + self._show_errors, + ) def resetConfig(self): """Reset the configuration parameters.""" self._generate_config() # Use grompp to generate the portable binary run input file. - self._generate_binary_run_file() + self._generate_binary_run_file( + self._config_file, + self._gro_file, + self._top_file, + self._tpr_file, + self._exe, + self._checkpoint_file, + self._ignore_warnings, + self._show_errors, + ) def setConfig(self, config): """ @@ -556,7 +653,16 @@ def setConfig(self, config): super().setConfig(config) # Use grompp to generate the portable binary run input file. - self._generate_binary_run_file() + self._generate_binary_run_file( + self._config_file, + self._gro_file, + self._top_file, + self._tpr_file, + self._exe, + self._checkpoint_file, + self._ignore_warnings, + self._show_errors, + ) def start(self): """ diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py index 0173acab4..9a2f9f63a 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py @@ -1373,7 +1373,7 @@ def _initialise_runner(self, system): extra_lines=self._extra_lines, ) - if self._setup_only: + if self._setup_only and not self._engine == "GROMACS": del first_process else: processes.append(first_process) @@ -1462,30 +1462,19 @@ def _initialise_runner(self, system): f.write(line) mdp = new_dir + "/gromacs.mdp" - mdp_out = new_dir + "/gromacs.out.mdp" gro = new_dir + "/gromacs.gro" top = new_dir + "/gromacs.top" tpr = new_dir + "/gromacs.tpr" # Use grompp to generate the portable binary run input file. - command = "%s grompp -f %s -po %s -c %s -p %s -r %s -o %s" % ( - self._exe, + first_process._generate_binary_run_file( mdp, - mdp_out, gro, top, - gro, tpr, - ) - - # Run the command. If this worked for the first lambda value, - # then it should work for all others. - proc = _subprocess.run( - _Utils.command_split(command), - shell=False, - text=True, - stdout=_subprocess.PIPE, - stderr=_subprocess.PIPE, + first_process._exe, + ignore_warnings=self._ignore_warnings, + show_errors=self._show_errors, ) # Create a copy of the process and update the working diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py index 097f3074f..9be77cd62 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py @@ -512,40 +512,119 @@ def _generate_args(self): if isinstance(self._protocol, (_Protocol.Metadynamics, _Protocol.Steering)): self.setArg("-plumed", "plumed.dat") - def _generate_binary_run_file(self): - """Use grommp to generate the binary run input file.""" + @staticmethod + def _generate_binary_run_file( + mdp_file, + gro_file, + top_file, + tpr_file, + exe, + checkpoint_file=None, + ignore_warnings=False, + show_errors=True, + ): + """ + Use grommp to generate the binary run input file. + + Parameters + ---------- + + mdp_file : str + The path to the input mdp file. + + gro_file : str + The path to the input coordinate file. + + top_file : str + The path to the input topology file. + + tpr_file : str + The path to the output binary run file. + + exe : str + The path to the GROMACS executable. + + checkpoint_file : str + The path to a checkpoint file from a previous run. This can be used + to continue an existing simulation. Currently we only support the + use of checkpoint files for Equilibration protocols. + + ignore_warnings : bool + Whether to ignore warnings when generating the binary run file + with 'gmx grompp'. By default, these warnings are elevated to + errors and will halt the program. + + show_errors : bool + Whether to show warning/error messages when generating the binary + run file. + """ + + if not isinstance(mdp_file, str): + raise ValueError("'mdp_file' must be of type 'str'.") + if not _os.path.isfile(mdp_file): + raise IOError(f"'mdp_file' doesn't exist: '{mdp_file}'") + + if not isinstance(gro_file, str): + raise ValueError("'gro_file' must be of type 'str'.") + if not _os.path.isfile(gro_file): + raise IOError(f"'gro_file' doesn't exist: '{gro_file}'") + + if not isinstance(top_file, str): + raise ValueError("'top_file' must be of type 'str'.") + if not _os.path.isfile(top_file): + raise IOError(f"'top_file' doesn't exist: '{top_file}'") + + if not isinstance(tpr_file, str): + raise ValueError("'tpr_file' must be of type 'str'.") + + if not isinstance(exe, str): + raise ValueError("'exe' must be of type 'str'.") + if not _os.path.isfile(exe): + raise IOError(f"'exe' doesn't exist: '{exe}'") + + if checkpoint_file is not None: + if not isinstance(checkpoint_file, str): + raise ValueError("'checkpoint_file' must be of type 'str'.") + if not _os.path.isfile(checkpoint_file): + raise IOError(f"'checkpoint_file' doesn't exist: '{checkpoint_file}'") + + if not isinstance(ignore_warnings, bool): + raise ValueError("'ignore_warnings' must be of type 'bool'") + + if not isinstance(show_errors, bool): + raise ValueError("'show_errors' must be of type 'bool'") # Create the name of the output mdp file. mdp_out = ( - _os.path.dirname(self._config_file) - + "/%s.out.mdp" % _os.path.basename(self._config_file).split(".")[0] + _os.path.dirname(mdp_file) + + "/%s.out.mdp" % _os.path.basename(mdp_file).split(".")[0] ) # Use grompp to generate the portable binary run input file. - if self._checkpoint_file is not None: + if checkpoint_file is not None: command = "%s grompp -f %s -po %s -c %s -p %s -r %s -t %s -o %s" % ( - self._exe, - self._config_file, + exe, + mdp_file, mdp_out, - self._gro_file, - self._top_file, - self._ref_file, - self._checkpoint_file, - self._tpr_file, + gro_file, + top_file, + gro_file, + checkpoint_file, + tpr_file, ) else: command = "%s grompp -f %s -po %s -c %s -p %s -r %s -o %s" % ( - self._exe, - self._config_file, + exe, + mdp_file, mdp_out, - self._gro_file, - self._top_file, - self._ref_file, - self._tpr_file, + gro_file, + top_file, + gro_file, + tpr_file, ) - # Warnings don't trigger an error. Set to a suitably large number. - if self._ignore_warnings: + # Warnings don't trigger an error. + if ignore_warnings: command += " --maxwarn 9999" # Run the command. @@ -560,7 +639,7 @@ def _generate_binary_run_file(self): # Check that grompp ran successfully. if proc.returncode != 0: # Handle errors and warnings. - if self._show_errors: + if show_errors: # Capture errors and warnings from the grompp output. errors = [] warnings = [] @@ -622,14 +701,32 @@ def addToConfig(self, config): super().addToConfig(config) # Use grompp to generate the portable binary run input file. - self._generate_binary_run_file() + self._generate_binary_run_file( + self._config_file, + self._gro_file, + self._top_file, + self._tpr_file, + self._exe, + checkpoint_file=self._checkpoint_file, + ignore_warnings=self._ignore_warnings, + show_errors=self._show_errors, + ) def resetConfig(self): """Reset the configuration parameters.""" self._generate_config() # Use grompp to generate the portable binary run input file. - self._generate_binary_run_file() + self._generate_binary_run_file( + self._config_file, + self._gro_file, + self._top_file, + self._tpr_file, + self._exe, + checkpoint_file=self._checkpoint_file, + ignore_warnings=self._ignore_warnings, + show_errors=self._show_errors, + ) def setConfig(self, config): """ @@ -647,7 +744,16 @@ def setConfig(self, config): super().setConfig(config) # Use grompp to generate the portable binary run input file. - self._generate_binary_run_file() + self._generate_binary_run_file( + self._config_file, + self._gro_file, + self._top_file, + self._tpr_file, + self._exe, + checkpoint_file=self._checkpoint_file, + ignore_warnings=self._ignore_warnings, + show_errors=self._show_errors, + ) def start(self): """ From ddc5785affd121c72c971307ec9e9a39499686d2 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 5 Feb 2024 15:36:00 +0000 Subject: [PATCH 007/121] Allow use of a separate file as a reference for position restraints. --- python/BioSimSpace/FreeEnergy/_relative.py | 1 + python/BioSimSpace/Process/_gromacs.py | 17 +++++++++++++++-- .../FreeEnergy/_alchemical_free_energy.py | 1 + .../Sandpit/Exscientia/Process/_gromacs.py | 17 +++++++++++++++-- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/python/BioSimSpace/FreeEnergy/_relative.py b/python/BioSimSpace/FreeEnergy/_relative.py index 44d7375d9..3a5eddff2 100644 --- a/python/BioSimSpace/FreeEnergy/_relative.py +++ b/python/BioSimSpace/FreeEnergy/_relative.py @@ -2146,6 +2146,7 @@ def _initialise_runner(self, system): mdp, gro, top, + gro, tpr, first_process._exe, ignore_warnings=self._ignore_warnings, diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index 3e9bec127..3e8239ad5 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -426,6 +426,7 @@ def _generate_binary_run_file( mdp_file, gro_file, top_file, + ref_file, tpr_file, exe, checkpoint_file=None, @@ -447,6 +448,10 @@ def _generate_binary_run_file( top_file : str The path to the input topology file. + ref_file : str + The path to the input reference coordinate file to be used for + position restraints. + tpr_file : str The path to the output binary run file. @@ -483,6 +488,11 @@ def _generate_binary_run_file( if not _os.path.isfile(top_file): raise IOError(f"'top_file' doesn't exist: '{top_file}'") + if not isinstance(ref_file, str): + raise ValueError("'ref_file' must be of type 'str'.") + if not _os.path.isfile(ref_file): + raise IOError(f"'ref_file' doesn't exist: '{ref_file}'") + if not isinstance(tpr_file, str): raise ValueError("'tpr_file' must be of type 'str'.") @@ -517,7 +527,7 @@ def _generate_binary_run_file( mdp_out, gro_file, top_file, - gro_file, + ref_file, checkpoint_file, tpr_file, ) @@ -528,7 +538,7 @@ def _generate_binary_run_file( mdp_out, gro_file, top_file, - gro_file, + ref_file, tpr_file, ) @@ -614,6 +624,7 @@ def addToConfig(self, config): self._config_file, self._gro_file, self._top_file, + self._gro_file, self._tpr_file, self._exe, self._checkpoint_file, @@ -630,6 +641,7 @@ def resetConfig(self): self._config_file, self._gro_file, self._top_file, + self._gro_file, self._tpr_file, self._exe, self._checkpoint_file, @@ -657,6 +669,7 @@ def setConfig(self, config): self._config_file, self._gro_file, self._top_file, + self._gro_file, self._tpr_file, self._exe, self._checkpoint_file, diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py index 9a2f9f63a..afcce7eff 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py @@ -1471,6 +1471,7 @@ def _initialise_runner(self, system): mdp, gro, top, + gro, tpr, first_process._exe, ignore_warnings=self._ignore_warnings, diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py index 9be77cd62..303a22764 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py @@ -517,6 +517,7 @@ def _generate_binary_run_file( mdp_file, gro_file, top_file, + ref_file, tpr_file, exe, checkpoint_file=None, @@ -538,6 +539,10 @@ def _generate_binary_run_file( top_file : str The path to the input topology file. + ref_file : str + The path to the input reference coordinate file to be used for + position restraints. + tpr_file : str The path to the output binary run file. @@ -574,6 +579,11 @@ def _generate_binary_run_file( if not _os.path.isfile(top_file): raise IOError(f"'top_file' doesn't exist: '{top_file}'") + if not isinstance(ref_file, str): + raise ValueError("'ref_file' must be of type 'str'.") + if not _os.path.isfile(ref_file): + raise IOError(f"'ref_file' doesn't exist: '{ref_file}'") + if not isinstance(tpr_file, str): raise ValueError("'tpr_file' must be of type 'str'.") @@ -608,7 +618,7 @@ def _generate_binary_run_file( mdp_out, gro_file, top_file, - gro_file, + ref_file, checkpoint_file, tpr_file, ) @@ -619,7 +629,7 @@ def _generate_binary_run_file( mdp_out, gro_file, top_file, - gro_file, + ref_file, tpr_file, ) @@ -705,6 +715,7 @@ def addToConfig(self, config): self._config_file, self._gro_file, self._top_file, + self._ref_file, self._tpr_file, self._exe, checkpoint_file=self._checkpoint_file, @@ -721,6 +732,7 @@ def resetConfig(self): self._config_file, self._gro_file, self._top_file, + self._ref_file, self._tpr_file, self._exe, checkpoint_file=self._checkpoint_file, @@ -748,6 +760,7 @@ def setConfig(self, config): self._config_file, self._gro_file, self._top_file, + self._ref_file, self._tpr_file, self._exe, checkpoint_file=self._checkpoint_file, From 62763b65c6f59612269f15bfdf71eebb0cf07687 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 5 Feb 2024 20:01:48 +0000 Subject: [PATCH 008/121] Run pytest-black against Python 3.11. --- recipes/biosimspace/template.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recipes/biosimspace/template.yaml b/recipes/biosimspace/template.yaml index 50b670ded..2ee59d5f8 100644 --- a/recipes/biosimspace/template.yaml +++ b/recipes/biosimspace/template.yaml @@ -27,8 +27,8 @@ test: - SIRE_SILENT_PHONEHOME requires: - pytest - - black 23 # [linux and x86_64 and py==39] - - pytest-black # [linux and x86_64 and py==39] + - black 23 # [linux and x86_64 and py==311] + - pytest-black # [linux and x86_64 and py==311] - ambertools # [linux and x86_64] - gromacs # [linux and x86_64] imports: From d18da44b775026df40ec7721484ad09a2e5b81a2 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 6 Feb 2024 09:42:45 +0000 Subject: [PATCH 009/121] Forgot to use selector for running pytest-black. --- recipes/biosimspace/template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/biosimspace/template.yaml b/recipes/biosimspace/template.yaml index 2ee59d5f8..1703158b3 100644 --- a/recipes/biosimspace/template.yaml +++ b/recipes/biosimspace/template.yaml @@ -37,7 +37,7 @@ test: - python/BioSimSpace # [linux and x86_64 and py==39] - tests commands: - - pytest -vvv --color=yes --black python/BioSimSpace # [linux and x86_64 and py==39] + - pytest -vvv --color=yes --black python/BioSimSpace # [linux and x86_64 and py==311] - pytest -vvv --color=yes --import-mode=importlib tests about: From 4998dd2029eb0de1391c67746e46efa92ab0ec84 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 6 Feb 2024 10:53:29 +0000 Subject: [PATCH 010/121] Also need to add source files to selector. --- recipes/biosimspace/template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/biosimspace/template.yaml b/recipes/biosimspace/template.yaml index 1703158b3..121765664 100644 --- a/recipes/biosimspace/template.yaml +++ b/recipes/biosimspace/template.yaml @@ -34,7 +34,7 @@ test: imports: - BioSimSpace source_files: - - python/BioSimSpace # [linux and x86_64 and py==39] + - python/BioSimSpace # [linux and x86_64 and py==311] - tests commands: - pytest -vvv --color=yes --black python/BioSimSpace # [linux and x86_64 and py==311] From 4175229453cdfc5e4715bd8577165da91d71d7bb Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 6 Feb 2024 12:28:39 +0000 Subject: [PATCH 011/121] Pin to pytest 7 until pytest-black is updated. --- recipes/biosimspace/template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/biosimspace/template.yaml b/recipes/biosimspace/template.yaml index 121765664..940f55cc1 100644 --- a/recipes/biosimspace/template.yaml +++ b/recipes/biosimspace/template.yaml @@ -26,7 +26,7 @@ test: - SIRE_DONT_PHONEHOME - SIRE_SILENT_PHONEHOME requires: - - pytest + - pytest <8 - black 23 # [linux and x86_64 and py==311] - pytest-black # [linux and x86_64 and py==311] - ambertools # [linux and x86_64] From b7944d11fe59c7fbd2754bda093ec83e9d8dc27f Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 7 Feb 2024 09:06:31 +0000 Subject: [PATCH 012/121] Call staticmethod on class, not instance. --- python/BioSimSpace/FreeEnergy/_relative.py | 2 +- .../Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/FreeEnergy/_relative.py b/python/BioSimSpace/FreeEnergy/_relative.py index 3a5eddff2..8db00c5a8 100644 --- a/python/BioSimSpace/FreeEnergy/_relative.py +++ b/python/BioSimSpace/FreeEnergy/_relative.py @@ -2142,7 +2142,7 @@ def _initialise_runner(self, system): tpr = new_dir + "/gromacs.tpr" # Use grompp to generate the portable binary run input file. - first_process._generate_binary_run_file( + _Process.Gromacs._generate_binary_run_file( mdp, gro, top, diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py index afcce7eff..104408902 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_alchemical_free_energy.py @@ -1467,7 +1467,7 @@ def _initialise_runner(self, system): tpr = new_dir + "/gromacs.tpr" # Use grompp to generate the portable binary run input file. - first_process._generate_binary_run_file( + _Process.Gromacs._generate_binary_run_file( mdp, gro, top, From 6ad3ac1e23a3d9ee9747fbe7638ed770442a285d Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 12 Feb 2024 09:50:18 +0000 Subject: [PATCH 013/121] Build Python 3.10 for macOS. --- .github/workflows/devel.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index c94a3cc27..ce34c30cc 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -25,9 +25,6 @@ jobs: python-version: "3.9" - platform: { name: "windows", os: "windows-latest", shell: "pwsh" } python-version: "3.9" - - platform: - { name: "macos", os: "macos-latest", shell: "bash -l {0}" } - python-version: "3.10" - platform: { name: "windows", os: "windows-latest", shell: "pwsh" } python-version: "3.10" environment: From bec58fb6db3299a31880010b112b430910953a9a Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 14 Feb 2024 11:44:00 +0000 Subject: [PATCH 014/121] Update support range. [ci skip] --- SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index da13da85c..96ba0bfe1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,8 +9,8 @@ which will be released as 2023.1.X+1. | Version | Supported | | ------- | ------------------ | -| 2023.1.x | :white_check_mark: | -| < 2023.1.x| :x: | +| 2023.5.x | :white_check_mark: | +| < 2023.5.x| :x: | ## Reporting a Vulnerability From 5cc7dea973b4ebb3f664592608643f30dd844ee2 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 19 Feb 2024 15:50:12 +0000 Subject: [PATCH 015/121] Remove redundant parmed import and references to parmed files. [closes #242] --- python/BioSimSpace/Parameters/_Protocol/_openforcefield.py | 7 +------ .../Exscientia/Parameters/_Protocol/_openforcefield.py | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py index 9571e79ce..bd86f5371 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py @@ -37,7 +37,6 @@ import os as _os -_parmed = _try_import("parmed") import queue as _queue import subprocess as _subprocess @@ -189,10 +188,6 @@ def run(self, molecule, work_dir=None, queue=None): else: is_smiles = False - # The following is adapted from the Open Force Field examples, where an - # OpenFF system is converted to AMBER format files using ParmEd: - # https://github.com/openforcefield/openff-toolkit/blob/master/examples/using_smirnoff_in_amber_or_gromacs/convert_to_amber_gromacs.ipynb - if is_smiles: # Convert SMILES string to an OpenFF molecule. try: @@ -353,7 +348,7 @@ def run(self, molecule, work_dir=None, queue=None): if par_mol.nMolecules() == 1: par_mol = par_mol.getMolecules()[0] except Exception as e: - msg = "Failed to read molecule from: 'parmed.prmtop', 'parmed.inpcrd'" + msg = "Failed to read molecule from: 'interchange.prmtop', 'interchange.inpcrd'" if _isVerbose(): msg += ": " + getattr(e, "message", repr(e)) raise IOError(msg) from e diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py index 9571e79ce..bd86f5371 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py @@ -37,7 +37,6 @@ import os as _os -_parmed = _try_import("parmed") import queue as _queue import subprocess as _subprocess @@ -189,10 +188,6 @@ def run(self, molecule, work_dir=None, queue=None): else: is_smiles = False - # The following is adapted from the Open Force Field examples, where an - # OpenFF system is converted to AMBER format files using ParmEd: - # https://github.com/openforcefield/openff-toolkit/blob/master/examples/using_smirnoff_in_amber_or_gromacs/convert_to_amber_gromacs.ipynb - if is_smiles: # Convert SMILES string to an OpenFF molecule. try: @@ -353,7 +348,7 @@ def run(self, molecule, work_dir=None, queue=None): if par_mol.nMolecules() == 1: par_mol = par_mol.getMolecules()[0] except Exception as e: - msg = "Failed to read molecule from: 'parmed.prmtop', 'parmed.inpcrd'" + msg = "Failed to read molecule from: 'interchange.prmtop', 'interchange.inpcrd'" if _isVerbose(): msg += ": " + getattr(e, "message", repr(e)) raise IOError(msg) from e From 17307238dffa0cd83875b27024c4ae384c89124f Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 19 Feb 2024 15:51:59 +0000 Subject: [PATCH 016/121] Expose FreeEnergyMixin protocols. [closes #243] --- python/BioSimSpace/Process/_amber.py | 3 ++- python/BioSimSpace/Process/_gromacs.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index fd8123fa0..0f19e0e16 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -46,6 +46,7 @@ from .._Config import Amber as _AmberConfig from .._Exceptions import IncompatibleError as _IncompatibleError from .._Exceptions import MissingSoftwareError as _MissingSoftwareError +from ..Protocol._free_energy_mixin import _FreeEnergyMixin from ..Protocol._position_restraint_mixin import _PositionRestraintMixin from .._SireWrappers import System as _System from ..Types._type import Type as _Type @@ -126,7 +127,7 @@ def __init__( ) # Catch unsupported protocols. - if isinstance(protocol, _Protocol.FreeEnergy): + if isinstance(protocol, _FreeEnergyMixin): raise _IncompatibleError( "Unsupported protocol: '%s'" % self._protocol.__class__.__name__ ) diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index 3e8239ad5..9478e4445 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -52,6 +52,7 @@ from .. import _isVerbose from .._Config import Gromacs as _GromacsConfig from .._Exceptions import MissingSoftwareError as _MissingSoftwareError +from ..Protocol._free_energy_mixin import _FreeEnergyMixin from ..Protocol._position_restraint_mixin import _PositionRestraintMixin from .._SireWrappers import System as _System from ..Types._type import Type as _Type @@ -232,7 +233,7 @@ def _setup(self): # Create a copy of the system. system = self._system.copy() - if isinstance(self._protocol, _Protocol.FreeEnergy): + if isinstance(self._protocol, _FreeEnergyMixin): # Check that the system contains a perturbable molecule. if self._system.nPerturbableMolecules() == 0: raise ValueError( From 1cfa0bdf54a21f9400c4645b640f7805a31923a0 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 19 Feb 2024 16:06:33 +0000 Subject: [PATCH 017/121] Remove shared properties when loading perturbable systems. [closes #241] --- python/BioSimSpace/IO/_io.py | 20 +++++++++++++- python/BioSimSpace/Process/_gromacs.py | 26 +++++++++++++------ .../BioSimSpace/Sandpit/Exscientia/IO/_io.py | 20 +++++++++++++- .../Sandpit/Exscientia/Process/_gromacs.py | 26 +++++++++++++------ 4 files changed, 74 insertions(+), 18 deletions(-) diff --git a/python/BioSimSpace/IO/_io.py b/python/BioSimSpace/IO/_io.py index 2f0ddb417..3fcf08e23 100644 --- a/python/BioSimSpace/IO/_io.py +++ b/python/BioSimSpace/IO/_io.py @@ -566,7 +566,7 @@ def readMolecules( prop = property_map.get("time", "time") time = system.property(prop) system.removeSharedProperty(prop) - system.setProperties(prop, time) + system.setProperty(prop, time) except: pass @@ -1148,6 +1148,24 @@ def readPerturbableSystem(top0, coords0, top1, coords1, property_map={}): # Update the molecule in the original system. system0.updateMolecules(mol) + # Remove "space" and "time" shared properties since this causes incorrect + # behaviour when extracting molecules and recombining them to make other + # systems. + try: + # Space. + prop = property_map.get("space", "space") + space = system0._sire_object.property(prop) + system0._sire_object.removeSharedProperty(prop) + system0._sire_object.setProperty(prop, space) + + # Time. + prop = property_map.get("time", "time") + time = system0._sire_object.property(prop) + system0._sire_object.removeSharedProperty(prop) + system0._sire_object.setProperty(prop, time) + except: + pass + return system0 diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index 9478e4445..e4531dad3 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -2545,10 +2545,15 @@ def _getFinalFrame(self): space_prop in old_system._sire_object.propertyKeys() and space_prop in new_system._sire_object.propertyKeys() ): - box = new_system._sire_object.property("space") - old_system._sire_object.setProperty( - self._property_map.get("space", "space"), box - ) + # Get the original space. + box = old_system._sire_object.property("space") + + # Only update the box if the space is periodic. + if box.isPeriodic(): + box = new_system._sire_object.property("space") + old_system._sire_object.setProperty( + self._property_map.get("space", "space"), box + ) # If this is a vacuum simulation, then translate the centre of mass # of the system back to the origin. @@ -2656,11 +2661,16 @@ def _getFrame(self, time): space_prop in old_system._sire_object.propertyKeys() and space_prop in new_system._sire_object.propertyKeys() ): - box = new_system._sire_object.property("space") + # Get the original space. + box = old_system._sire_object.property("space") + + # Only update the box if the space is periodic. if box.isPeriodic(): - old_system._sire_object.setProperty( - self._property_map.get("space", "space"), box - ) + box = new_system._sire_object.property("space") + if box.isPeriodic(): + old_system._sire_object.setProperty( + self._property_map.get("space", "space"), box + ) # If this is a vacuum simulation, then translate the centre of mass # of the system back to the origin. diff --git a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py index 2f0ddb417..97ac66348 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py +++ b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py @@ -566,7 +566,7 @@ def readMolecules( prop = property_map.get("time", "time") time = system.property(prop) system.removeSharedProperty(prop) - system.setProperties(prop, time) + system.setProperty(prop, time) except: pass @@ -1148,6 +1148,24 @@ def readPerturbableSystem(top0, coords0, top1, coords1, property_map={}): # Update the molecule in the original system. system0.updateMolecules(mol) + # Remove "space" and "time" shared properties since this causes incorrect + # behaviour when extracting molecules and recombining them to make other + # systems. + try: + # Space. + prop = property_map.get("space", "space") + space = system0._sire_object.property(prop) + system0._sire_object.removeSharedProperty(prop) + system0._sire_object.setProperty(prop, space) + + # Time. + prop = property_map.get("time", "time") + time = system0._sire_object.property(prop) + system0._sire_object.removeSharedProperty(prop) + system0._sire_object.setPropery(prop, time) + except: + pass + return system0 diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py index 303a22764..6d0bf4278 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py @@ -2638,10 +2638,15 @@ def _getFinalFrame(self): space_prop in old_system._sire_object.propertyKeys() and space_prop in new_system._sire_object.propertyKeys() ): - box = new_system._sire_object.property("space") - old_system._sire_object.setProperty( - self._property_map.get("space", "space"), box - ) + # Get the original space. + box = old_system._sire_object.property("space") + + # Only update the box if the space is periodic. + if box.isPeriodic(): + box = new_system._sire_object.property("space") + old_system._sire_object.setProperty( + self._property_map.get("space", "space"), box + ) # If this is a vacuum simulation, then translate the centre of mass # of the system back to the origin. @@ -2749,11 +2754,16 @@ def _getFrame(self, time): space_prop in old_system._sire_object.propertyKeys() and space_prop in new_system._sire_object.propertyKeys() ): - box = new_system._sire_object.property("space") + # Get the original space. + box = old_system._sire_object.property("space") + + # Only update the box if the space is periodic. if box.isPeriodic(): - old_system._sire_object.setProperty( - self._property_map.get("space", "space"), box - ) + box = new_system._sire_object.property("space") + if box.isPeriodic(): + old_system._sire_object.setProperty( + self._property_map.get("space", "space"), box + ) # If this is a vacuum simulation, then translate the centre of mass # of the system back to the origin. From 7ab5c8df7222561d90dcacba5f92b7c57947504e Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 21 Feb 2024 14:13:05 +0000 Subject: [PATCH 018/121] Add support for using a reference system for position restraints. --- python/BioSimSpace/Process/_amber.py | 27 ++++++++++++++++- python/BioSimSpace/Process/_gromacs.py | 26 +++++++++++++++-- python/BioSimSpace/Process/_namd.py | 11 ++++++- python/BioSimSpace/Process/_openmm.py | 40 ++++++++++++++++++++++---- python/BioSimSpace/Process/_process.py | 27 +++++++++++++++++ 5 files changed, 121 insertions(+), 10 deletions(-) diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index 0f19e0e16..c5840d7f5 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -69,6 +69,7 @@ def __init__( self, system, protocol, + reference_system=None, exe=None, name="amber", work_dir=None, @@ -89,6 +90,11 @@ def __init__( protocol : :class:`Protocol ` The protocol for the AMBER process. + reference_system : :class:`System ` or None + An optional system to use as a source of reference coordinates for position + restraints. It is assumed that this system has the same topology as "system". + If this is None, then "system" is used as a reference. + exe : str The full path to the AMBER executable. @@ -118,6 +124,7 @@ def __init__( super().__init__( system, protocol, + reference_system=reference_system, name=name, work_dir=work_dir, seed=seed, @@ -172,6 +179,7 @@ def __init__( # The names of the input files. self._rst_file = "%s/%s.rst7" % (self._work_dir, name) self._top_file = "%s/%s.prm7" % (self._work_dir, name) + self._ref_file = "%s/%s_ref.rst7" % (self._work_dir, name) # The name of the trajectory file. self._traj_file = "%s/%s.nc" % (self._work_dir, name) @@ -182,6 +190,10 @@ def __init__( # Create the list of input files. self._input_files = [self._config_file, self._rst_file, self._top_file] + # Add the reference file if there are position restraints. + if self._protocol.getRestraint() is not None: + self._input_files.append(self._ref_file) + # Now set up the working directory for the process. self._setup() @@ -210,6 +222,19 @@ def _setup(self): else: raise IOError(msg) from None + # Reference file for position restraints. + try: + file = _os.path.splitext(self._ref_file)[0] + _IO.saveMolecules( + file, self._reference_system, "rst7", property_map=self._property_map + ) + except Exception as e: + msg = "Failed to write reference system to 'RST7' format." + if _isVerbose(): + raise IOError(msg) from e + else: + raise IOError(msg) from None + # PRM file (topology). try: file = _os.path.splitext(self._top_file)[0] @@ -315,7 +340,7 @@ def _generate_args(self): # Append a reference file if a position restraint is specified. if isinstance(self._protocol, _PositionRestraintMixin): if self._protocol.getRestraint() is not None: - self.setArg("-ref", "%s.rst7" % self._name) + self.setArg("-ref", self._ref_file) # Append a trajectory file if this anything other than a minimisation. if not isinstance(self._protocol, _Protocol.Minimisation): diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index e4531dad3..7c22f60e0 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -76,6 +76,7 @@ def __init__( self, system, protocol, + reference_system=None, exe=None, name="gromacs", work_dir=None, @@ -99,6 +100,11 @@ def __init__( protocol : :class:`Protocol ` The protocol for the GROMACS process. + reference_system : :class:`System ` or None + An optional system to use as a source of reference coordinates for position + restraints. It is assumed that this system has the same topology as "system". + If this is None, then "system" is used as a reference. + exe : str The full path to the GROMACS executable. @@ -142,6 +148,7 @@ def __init__( super().__init__( system, protocol, + reference_system=reference_system, name=name, work_dir=work_dir, seed=seed, @@ -193,6 +200,7 @@ def __init__( # The names of the input files. self._gro_file = "%s/%s.gro" % (self._work_dir, name) self._top_file = "%s/%s.top" % (self._work_dir, name) + self._ref_file = "%s/%s_ref.gro" % (self._work_dir, name) # The name of the trajectory file. self._traj_file = "%s/%s.trr" % (self._work_dir, name) @@ -206,6 +214,10 @@ def __init__( # Create the list of input files. self._input_files = [self._config_file, self._gro_file, self._top_file] + # Add the reference file if there are position restraints. + if self._protocol.getRestraint() is not None: + self._input_files.append(self._ref_file) + # Initialise the PLUMED interface object. self._plumed = None @@ -263,6 +275,16 @@ def _setup(self): file, system, "gro87", match_water=False, property_map=self._property_map ) + # Reference file. + file = _os.path.splitext(self._ref_file)[0] + _IO.saveMolecules( + file, + self._reference_system, + "gro87", + match_water=False, + property_map=self._property_map, + ) + # TOP file. file = _os.path.splitext(self._top_file)[0] _IO.saveMolecules( @@ -1992,8 +2014,8 @@ def _add_position_restraints(self): property_map["parallel"] = _SireBase.wrap(False) property_map["sort"] = _SireBase.wrap(False) - # Create a copy of the system. - system = self._system.copy() + # Create a copy of the reference system. + system = self._reference_system.copy() # Convert to the lambda = 0 state if this is a perturbable system. system = self._checkPerturbable(system) diff --git a/python/BioSimSpace/Process/_namd.py b/python/BioSimSpace/Process/_namd.py index 25ecaa5f3..f996c9555 100644 --- a/python/BioSimSpace/Process/_namd.py +++ b/python/BioSimSpace/Process/_namd.py @@ -63,6 +63,7 @@ def __init__( self, system, protocol, + reference_system=None, exe=None, name="namd", work_dir=None, @@ -81,6 +82,11 @@ def __init__( protocol : :class:`Protocol ` The protocol for the NAMD process. + reference_system : :class:`System ` or None + An optional system to use as a source of reference coordinates for position + restraints. It is assumed that this system has the same topology as "system". + If this is None, then "system" is used as a reference. + exe : str The full path to the NAMD executable. @@ -103,6 +109,7 @@ def __init__( super().__init__( system, protocol, + reference_system=reference_system, name=name, work_dir=work_dir, seed=seed, @@ -421,7 +428,9 @@ def _generate_config(self): restraint = self._protocol.getRestraint() if restraint is not None: # Create a restrained system. - restrained = self._createRestrainedSystem(self._system, restraint) + restrained = self._createRestrainedSystem( + self._reference_system, restraint + ) # Create a PDB object, mapping the "occupancy" property to "restrained". prop = self._property_map.get("occupancy", "occupancy") diff --git a/python/BioSimSpace/Process/_openmm.py b/python/BioSimSpace/Process/_openmm.py index 557bef80e..76bb8451a 100644 --- a/python/BioSimSpace/Process/_openmm.py +++ b/python/BioSimSpace/Process/_openmm.py @@ -72,6 +72,7 @@ def __init__( self, system, protocol, + reference_system=None, exe=None, name="openmm", platform="CPU", @@ -91,6 +92,11 @@ def __init__( protocol : :class:`Protocol ` The protocol for the OpenMM process. + reference_system : :class:`System ` or None + An optional system to use as a source of reference coordinates for position + restraints. It is assumed that this system has the same topology as "system". + If this is None, then "system" is used as a reference. + exe : str The full path to the Python interpreter used to run OpenMM. @@ -120,6 +126,7 @@ def __init__( super().__init__( system, protocol, + reference_system=reference_system, name=name, work_dir=work_dir, seed=seed, @@ -175,6 +182,7 @@ def __init__( # are self-contained, but could equally work with GROMACS files. self._rst_file = "%s/%s.rst7" % (self._work_dir, name) self._top_file = "%s/%s.prm7" % (self._work_dir, name) + self._ref_file = "%s/%s_ref.rst7" % (self._work_dir, name) # The name of the trajectory file. self._traj_file = "%s/%s.dcd" % (self._work_dir, name) @@ -186,6 +194,10 @@ def __init__( # Create the list of input files. self._input_files = [self._config_file, self._rst_file, self._top_file] + # Add the reference file if there are position restraints. + if self._protocol.getRestraint() is not None: + self._input_files.append(self._ref_file) + # Initialise the log file header. self._header = None @@ -249,6 +261,18 @@ def _setup(self): else: raise IOError(msg) from None + # Reference coordinate file for position restraints. + if self._protocol.getRestraint() is not None: + try: + file = _os.path.splitext(self._ref_file)[0] + _IO.saveMolecules(file, system, "rst7", property_map=self._property_map) + except Exception as e: + msg = "Failed to write reference system to 'RST7' format." + if _isVerbose(): + raise IOError(msg) from e + else: + raise IOError(msg) from None + # PRM file (topology). try: file = _os.path.splitext(self._top_file)[0] @@ -308,7 +332,7 @@ def _generate_config(self): "\n# We use ParmEd due to issues with the built in AmberPrmtopFile for certain triclinic spaces." ) self.addToConfig( - f"prm = parmed.load_file('{self._name}.prm7', '{self._name}.rst7')" + f"prm = parmed.load_file('{self._top_file}', '{self._rst_file}')" ) # Don't use a cut-off if this is a vacuum simulation or if box information @@ -377,7 +401,7 @@ def _generate_config(self): "\n# We use ParmEd due to issues with the built in AmberPrmtopFile for certain triclinic spaces." ) self.addToConfig( - f"prm = parmed.load_file('{self._name}.prm7', '{self._name}.rst7')" + f"prm = parmed.load_file('{self._top_file}', '{self._rst_file}')" ) # Don't use a cut-off if this is a vacuum simulation or if box information @@ -561,7 +585,7 @@ def _generate_config(self): "\n# We use ParmEd due to issues with the built in AmberPrmtopFile for certain triclinic spaces." ) self.addToConfig( - f"prm = parmed.load_file('{self._name}.prm7', '{self._name}.rst7')" + f"prm = parmed.load_file('{self._top_file}', '{self._rst_file}')" ) # Don't use a cut-off if this is a vacuum simulation or if box information @@ -760,7 +784,7 @@ def _generate_config(self): "\n# We use ParmEd due to issues with the built in AmberPrmtopFile for certain triclinic spaces." ) self.addToConfig( - f"prm = parmed.load_file('{self._name}.prm7', '{self._name}.rst7')" + f"prm = parmed.load_file('{self._top_file}', '{self._rst_file}')" ) # Don't use a cut-off if this is a vacuum simulation or if box information @@ -2140,11 +2164,15 @@ def _add_config_restraints(self): if restraint is not None: # Search for the atoms to restrain by keyword. if isinstance(restraint, str): - restrained_atoms = self._system.getRestraintAtoms(restraint) + restrained_atoms = self._reference_system.getRestraintAtoms(restraint) # Use the user-defined list of indices. else: restrained_atoms = restraint + self.addToConfig( + f"ref_prm = parmed.load_file('{self._top_file}', '{self._rst_file}')" + ) + # Get the force constant in units of kJ_per_mol/nanometer**2 force_constant = self._protocol.getForceConstant()._sire_unit force_constant = force_constant.to( @@ -2161,7 +2189,7 @@ def _add_config_restraints(self): "nonbonded = [f for f in system.getForces() if isinstance(f, NonbondedForce)][0]" ) self.addToConfig("dummy_indices = []") - self.addToConfig("positions = prm.positions") + self.addToConfig("positions = ref_prm.positions") self.addToConfig(f"restrained_atoms = {restrained_atoms}") self.addToConfig("for i in restrained_atoms:") self.addToConfig(" j = system.addParticle(0)") diff --git a/python/BioSimSpace/Process/_process.py b/python/BioSimSpace/Process/_process.py index 255dee9df..1865b128d 100644 --- a/python/BioSimSpace/Process/_process.py +++ b/python/BioSimSpace/Process/_process.py @@ -70,6 +70,7 @@ def __init__( self, system, protocol, + reference_system=None, name=None, work_dir=None, seed=None, @@ -89,6 +90,11 @@ def __init__( protocol : :class:`Protocol ` The protocol for the process. + reference_system : :class:`System ` or None + An optional system to use as a source of reference coordinates for position + restraints. It is assumed that this system has the same topology as "system". + If this is None, then "system" is used as a reference. + name : str The name of the process. @@ -137,6 +143,27 @@ def __init__( if not isinstance(protocol, _Protocol): raise TypeError("'protocol' must be of type 'BioSimSpace.Protocol'") + # Check that the reference system is valid. + if reference_system is not None: + if not isinstance(reference_system, _System): + raise TypeError( + "'reference_system' must be of type 'BioSimSpace._SireWrappers.System'" + ) + + # Make sure that the reference system contains the same number + # of molecules, residues, and atoms as the system. + if ( + not reference_system.nMolecules() == system.nMolecules() + or not reference_system.nResidues() == system.nResidues() + or not reference_system.nAtoms() == system.nAtoms() + ): + raise _IncompatibleError( + "'refence_system' must have the same topology as 'system'" + ) + self._reference_system = reference_system + else: + self._reference_system = system.copy() + # Check that the working directory is valid. if work_dir is not None and not isinstance(work_dir, (str, _Utils.WorkDir)): raise TypeError( From 5dc399ad6ff4121d8a1e3a6d08717e07f6cd22db Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 21 Feb 2024 16:15:29 +0000 Subject: [PATCH 019/121] Add support for FEP with AMBER molecular dynamics engine. --- python/BioSimSpace/Align/_merge.py | 78 + python/BioSimSpace/Align/_squash.py | 725 ++ python/BioSimSpace/FreeEnergy/_relative.py | 89 +- python/BioSimSpace/Process/_amber.py | 1193 ++- .../Sandpit/Exscientia/Process/_amber.py | 58 +- python/BioSimSpace/_Config/_amber.py | 131 +- python/BioSimSpace/_SireWrappers/_molecule.py | 23 +- tests/Align/test_squash.py | 204 + tests/Process/test_amber.py | 74 + tests/input/merged_tripeptide.pickle | Bin 0 -> 65668 bytes tests/output/amber_fep.out | 6720 +++++++++++++++++ tests/output/amber_fep_min.out | 2059 +++++ 12 files changed, 11188 insertions(+), 166 deletions(-) create mode 100644 python/BioSimSpace/Align/_squash.py create mode 100644 tests/Align/test_squash.py create mode 100644 tests/input/merged_tripeptide.pickle create mode 100644 tests/output/amber_fep.out create mode 100644 tests/output/amber_fep_min.out diff --git a/python/BioSimSpace/Align/_merge.py b/python/BioSimSpace/Align/_merge.py index 9c2b96309..f43eb3a5e 100644 --- a/python/BioSimSpace/Align/_merge.py +++ b/python/BioSimSpace/Align/_merge.py @@ -27,6 +27,7 @@ __all__ = ["merge"] from sire.legacy import Base as _SireBase +from sire.legacy import IO as _SireIO from sire.legacy import MM as _SireMM from sire.legacy import Mol as _SireMol from sire.legacy import Units as _SireUnits @@ -1389,3 +1390,80 @@ def _is_on_ring(idx, conn): # If we get this far, then the atom is not adjacent to a ring. return False + + +def _removeDummies(molecule, is_lambda1): + """ + Internal function which removes the dummy atoms from one of the endstates + of a merged molecule. + + Parameters + ---------- + + molecule : BioSimSpace._SireWrappers.Molecule + The molecule. + + is_lambda1 : bool + Whether to use the molecule at lambda = 1. + """ + if not molecule._is_perturbable: + raise _IncompatibleError("'molecule' is not a perturbable molecule") + + # Always use the coordinates at lambda = 0. + coordinates = molecule._sire_object.property("coordinates0") + + # Generate a molecule with all dummies present. + molecule = molecule.copy()._toRegularMolecule( + is_lambda1=is_lambda1, generate_intrascale=True + ) + + # Set the coordinates to those at lambda = 0 + molecule._sire_object = ( + molecule._sire_object.edit().setProperty("coordinates", coordinates).commit() + ) + + # Extract all the nondummy indices + nondummy_indices = [ + i + for i, atom in enumerate(molecule.getAtoms()) + if "du" not in atom._sire_object.property("ambertype") + ] + + # Create an AtomSelection. + selection = molecule._sire_object.selection() + + # Unselect all of the atoms. + selection.selectNone() + + # Now add all of the nondummy atoms. + for idx in nondummy_indices: + selection.select(_SireMol.AtomIdx(idx)) + + # Create a partial molecule and extract the atoms. + partial_molecule = ( + _SireMol.PartialMolecule(molecule._sire_object, selection).extract().molecule() + ) + + # Remove the incorrect intrascale property. + partial_molecule = ( + partial_molecule.edit().removeProperty("intrascale").molecule().commit() + ) + + # Recreate a BioSimSpace molecule object. + molecule = _Molecule(partial_molecule) + + # Parse the molecule as a GROMACS topology, which will recover the intrascale + # matrix. + gro_top = _SireIO.GroTop(molecule.toSystem()._sire_object) + + # Convert back to a Sire system. + gro_sys = gro_top.toSystem() + + # Add the intrascale property back into the merged molecule. + edit_mol = molecule._sire_object.edit() + edit_mol = edit_mol.setProperty( + "intrascale", gro_sys[_SireMol.MolIdx(0)].property("intrascale") + ) + molecule = _Molecule(edit_mol.commit()) + + return molecule diff --git a/python/BioSimSpace/Align/_squash.py b/python/BioSimSpace/Align/_squash.py new file mode 100644 index 000000000..fa3d6ad79 --- /dev/null +++ b/python/BioSimSpace/Align/_squash.py @@ -0,0 +1,725 @@ +import itertools as _it +import numpy as _np +import os as _os +import parmed as _pmd +import shutil as _shutil +import tempfile + +from sire.legacy import IO as _SireIO +from sire.legacy import Mol as _SireMol + +from ._merge import _removeDummies +from ..IO import readMolecules as _readMolecules, saveMolecules as _saveMolecules +from .._SireWrappers import Molecule as _Molecule + + +def _squash(system, explicit_dummies=False): + """ + Internal function which converts a merged BioSimSpace system into an + AMBER-compatible format, where all perturbed molecules are represented + sequentially, instead of in a mixed topology, like in GROMACS. In the + current implementation, all perturbed molecules are moved at the end of + the squashed system. For example, if we have an input system, containing + regular molecules (M) and perturbed molecules (P): + + M0 - M1 - P0 - M2 - P1 - M3 + + This function will return the following squashed system: + + M0 - M1 - M2 - M3 - P0_A - PO_B - P1_A - P1_B + + Where A and B denote the dummyless lambda=0 and lambda=1 states. In + addition, we also return a mapping between the old unperturbed molecule + indices and the new ones. This mapping can be used during coordinate update. + Updating the coordinates of the perturbed molecules, however, has to be + done manually through the Python layer. + + Parameters + ---------- + + system : BioSimSpace._SireWrappers.System + The system. + + explicit_dummies : bool + Whether to keep the dummy atoms explicit at the endstates or remove them. + + Returns + ------- + + system : BioSimSpace._SireWrappers.System + The output squashed system. + + mapping : dict(sire.legacy.Mol.MolIdx, sire.legacy.Mol.MolIdx) + The corresponding molecule-to-molecule mapping. Only the non-perturbable + molecules are contained in this mapping as the perturbable ones do not + have a one-to-one mapping and cannot be expressed as a dictionary. + """ + # Create a copy of the original system. + new_system = system.copy() + + # Get the perturbable molecules and their corresponding indices. + pertmol_idxs = [ + i + for i, molecule in enumerate(system.getMolecules()) + if molecule.isPerturbable() + ] + pert_mols = system.getPerturbableMolecules() + + # Remove the perturbable molecules from the system. + new_system.removeMolecules(pert_mols) + + # Add them back at the end of the system. This is generally faster than + # keeping their order the same. + new_indices = list(range(system.nMolecules())) + for pertmol_idx, pert_mol in zip(pertmol_idxs, pert_mols): + new_indices.remove(pertmol_idx) + new_system += _squash_molecule(pert_mol, explicit_dummies=explicit_dummies) + + # Create the old molecule index to new molecule index mapping. + mapping = { + _SireMol.MolIdx(idx): _SireMol.MolIdx(i) for i, idx in enumerate(new_indices) + } + + return new_system, mapping + + +def _squash_molecule(molecule, explicit_dummies=False): + """ + This internal function converts a perturbed molecule to a system that is + recognisable to the AMBER alchemical code. If the molecule contains a single + residue, then the squashed system is just the two separate pure endstate + molecules in order. If the molecule contains regular (R) and perturbable (P) + resides of the form: + + R0 - R1 - P0 - R2 - P1 - R3 + + Then a system containing a single molecule will be returned, which is generated + by ParmEd's tiMerge as follows: + + R0 - R1 - P0_A - R2 - P1_A - R3 - P0_B - P1_B + + Where A and B denote the dummyless lambda=0 and lambda=1 states. + + Parameters + ---------- + + molecule : BioSimSpace._SireWrappers.Molecule + The input molecule. + + explicit_dummies : bool + Whether to keep the dummy atoms explicit at the endstates or remove them. + + Returns + ------- + + system : BioSimSpace._SireWrappers.System + The output squashed system. + """ + if not molecule.isPerturbable(): + return molecule + + if explicit_dummies: + # Get the common core atoms + atom_mapping0_common = _squashed_atom_mapping( + molecule, + is_lambda1=False, + explicit_dummies=explicit_dummies, + environment=False, + dummies=False, + ) + atom_mapping1_common = _squashed_atom_mapping( + molecule, + is_lambda1=True, + explicit_dummies=explicit_dummies, + environment=False, + dummies=False, + ) + if set(atom_mapping0_common) != set(atom_mapping1_common): + raise RuntimeError("The MCS atoms don't match between the two endstates") + common_atoms = set(atom_mapping0_common) + + # We make sure we use the same coordinates for the common core at both endstates. + c = molecule.copy()._sire_object.cursor() + for i, atom in enumerate(c.atoms()): + if i in common_atoms: + atom["coordinates1"] = atom["coordinates0"] + molecule = _Molecule(c.commit()) + + # Generate a "system" from the molecule at lambda = 0 and another copy at lambda = 1. + if explicit_dummies: + mol0 = molecule.copy()._toRegularMolecule( + is_lambda1=False, convert_amber_dummies=True, generate_intrascale=True + ) + mol1 = molecule.copy()._toRegularMolecule( + is_lambda1=True, convert_amber_dummies=True, generate_intrascale=True + ) + else: + mol0 = _removeDummies(molecule, False) + mol1 = _removeDummies(molecule, True) + system = (mol0 + mol1).toSystem() + + # We only need to call tiMerge for multi-residue molecules + if molecule.nResidues() == 1: + return system + + # Perform the multi-residue squashing with ParmEd as it is much easier and faster. + with tempfile.TemporaryDirectory() as tempdir: + # Load in ParmEd. + _saveMolecules(f"{tempdir}/temp", mol0 + mol1, "prm7,rst7") + _shutil.move(f"{tempdir}/temp.prm7", f"{tempdir}/temp.parm7") + parm = _pmd.load_file(f"{tempdir}/temp.parm7", xyz=f"{tempdir}/temp.rst7") + + # Determine the molecule masks. + mol_mask0 = f"@1-{mol0.nAtoms()}" + mol_mask1 = f"@{mol0.nAtoms() + 1}-{system.nAtoms()}" + + # Determine the residue masks. + atom0_offset, atom1_offset = 0, mol0.nAtoms() + res_atoms0, res_atoms1 = [], [] + for res0, res1, res01 in zip( + mol0.getResidues(), mol1.getResidues(), molecule.getResidues() + ): + if _is_perturbed(res01) or molecule.nResidues() == 1: + res_atoms0 += list(range(atom0_offset, atom0_offset + res0.nAtoms())) + res_atoms1 += list(range(atom1_offset, atom1_offset + res1.nAtoms())) + atom0_offset += res0.nAtoms() + atom1_offset += res1.nAtoms() + res_mask0 = _amber_mask_from_indices(res_atoms0) + res_mask1 = _amber_mask_from_indices(res_atoms1) + + # Merge the residues. + action = _pmd.tools.tiMerge(parm, mol_mask0, mol_mask1, res_mask0, res_mask1) + action.output = open(_os.devnull, "w") # Avoid some of the spam + action.execute() + + # Reload into BioSimSpace. + # TODO: prm7/rst7 doesn't work for some reason so we need to use gro/top + parm.save(f"{tempdir}/squashed.gro", overwrite=True) + parm.save(f"{tempdir}/squashed.top", overwrite=True) + squashed_mol = _readMolecules( + [f"{tempdir}/squashed.gro", f"{tempdir}/squashed.top"] + ) + + return squashed_mol + + +def _unsquash(system, squashed_system, mapping, **kwargs): + """ + Internal function which converts an alchemical AMBER system where the + perturbed molecules are defined sequentially and updates the coordinates + and velocities of an input unsquashed system. Refer to the _squash() + function documentation to see the structure of the squashed system + relative to the unsquashed one. + + Parameters + ---------- + + system : BioSimSpace._SireWrappers.System + The regular unsquashed system. + + squashed_system : BioSimSpace._SireWrappers.System + The corresponding squashed system. + + mapping : dict(sire.legacy.Mol.MolIdx, sire.legacy.Mol.MolIdx) + The molecule-molecule mapping generated by _squash(). + + kwargs : dict + A dictionary of optional keyword arguments to supply to _unsquash_molecule(). + + Returns + ------- + system : BioSimSpace._SireWrappers.System + The output unsquashed system. + """ + # Create a copy of the original new_system. + new_system = system.copy() + + # Update the unperturbed molecule coordinates in the original new_system + # using the mapping. + if mapping: + new_system._sire_object, _ = _SireIO.updateCoordinatesAndVelocities( + new_system._sire_object, squashed_system._sire_object, mapping + ) + + # From now on we handle all perturbed molecules. + pertmol_idxs = [ + i + for i, molecule in enumerate(new_system.getMolecules()) + if molecule.isPerturbable() + ] + + # Get the molecule mapping and combine it with the lambda=0 molecule + # being prioritised + molecule_mapping0 = _squashed_molecule_mapping(new_system, is_lambda1=False) + molecule_mapping1 = _squashed_molecule_mapping(new_system, is_lambda1=True) + molecule_mapping0_rev = {v: k for k, v in molecule_mapping0.items()} + molecule_mapping1_rev = {v: k for k, v in molecule_mapping1.items()} + molecule_mapping_rev = {**molecule_mapping1_rev, **molecule_mapping0_rev} + molecule_mapping_rev = { + k: v for k, v in molecule_mapping_rev.items() if v in pertmol_idxs + } + + # Update the perturbed molecule coordinates based on the molecule mapping + for merged_idx in set(molecule_mapping_rev.values()): + pertmol = new_system[merged_idx] + squashed_idx0 = molecule_mapping0[merged_idx] + squashed_idx1 = molecule_mapping1[merged_idx] + + if squashed_idx0 == squashed_idx1: + squashed_molecules = squashed_system[squashed_idx0].toSystem() + else: + squashed_molecules = ( + squashed_system[squashed_idx0] + squashed_system[squashed_idx1] + ).toSystem() + + new_pertmol = _unsquash_molecule(pertmol, squashed_molecules, **kwargs) + new_system.updateMolecule(merged_idx, new_pertmol) + + return new_system + + +def _unsquash_molecule(molecule, squashed_molecules, explicit_dummies=False): + """ + This internal function loads the coordinates and velocities of squashed + molecules as defined by the _squash_molecule() function into an unsquashed + merged molecule. + + Parameters + ---------- + + molecule : BioSimSpace._SireWrappers.Molecule + The unsquashed merged molecule whose coordinates and velocities are to be updated. + + squashed_molecules : BioSimSpace._SireWrappers.Molecules + The corresponding squashed molecule(s) whose coordinates are to be used for updating. + + explicit_dummies : bool + Whether to keep the dummy atoms explicit at the endstates or remove them. + + Returns + ------- + + molecule : BioSimSpace._SireWrappers.Molecule + The output updated merged molecule. + """ + # Get the common core atoms + atom_mapping0_common = _squashed_atom_mapping( + molecule, + is_lambda1=False, + explicit_dummies=explicit_dummies, + environment=False, + dummies=False, + ) + atom_mapping1_common = _squashed_atom_mapping( + molecule, + is_lambda1=True, + explicit_dummies=explicit_dummies, + environment=False, + dummies=False, + ) + if set(atom_mapping0_common) != set(atom_mapping1_common): + raise RuntimeError("The MCS atoms don't match between the two endstates") + common_atoms = set(atom_mapping0_common) + + # Get the atom mapping from both endstates + atom_mapping0 = _squashed_atom_mapping( + molecule, is_lambda1=False, explicit_dummies=explicit_dummies + ) + atom_mapping1 = _squashed_atom_mapping( + molecule, is_lambda1=True, explicit_dummies=explicit_dummies + ) + update_velocity = squashed_molecules[0]._sire_object.hasProperty("velocity") + + # Even though the common core of the two molecules should have the same coordinates, + # they might be PBC wrapped differently. + # Here we take the first common core atom and translate the second molecule. + if len(squashed_molecules) == 2: + first_common_atom = list(sorted(common_atoms))[0] + pertatom0 = squashed_molecules.getAtom(atom_mapping0[first_common_atom]) + pertatom1 = squashed_molecules.getAtom(atom_mapping1[first_common_atom]) + pertatom_coords0 = pertatom0._sire_object.property("coordinates") + pertatom_coords1 = pertatom1._sire_object.property("coordinates") + translation_vec = pertatom_coords1 - pertatom_coords0 + + # Update the coordinates and velocities. + siremol = molecule.copy()._sire_object.edit() + for merged_atom_idx in range(molecule.nAtoms()): + # Get the relevant atom indices + merged_atom = siremol.atom(_SireMol.AtomIdx(merged_atom_idx)) + if merged_atom_idx in atom_mapping0: + squashed_atom_idx0 = atom_mapping0[merged_atom_idx] + else: + squashed_atom_idx0 = atom_mapping1[merged_atom_idx] + if merged_atom_idx in atom_mapping1: + squashed_atom_idx1 = atom_mapping1[merged_atom_idx] + apply_translation_vec = True + else: + squashed_atom_idx1 = atom_mapping0[merged_atom_idx] + apply_translation_vec = False + + # Get the coordinates. + squashed_atom0 = squashed_molecules.getAtom(squashed_atom_idx0) + squashed_atom1 = squashed_molecules.getAtom(squashed_atom_idx1) + coordinates0 = squashed_atom0._sire_object.property("coordinates") + coordinates1 = squashed_atom1._sire_object.property("coordinates") + + # Apply the translation if the atom is coming from the second molecule. + if len(squashed_molecules) == 2 and apply_translation_vec: + # This is a dummy atom so we need to translate coordinates0 as well + if squashed_atom_idx0 == squashed_atom_idx1: + coordinates0 -= translation_vec + coordinates1 -= translation_vec + + siremol = merged_atom.setProperty("coordinates0", coordinates0).molecule() + siremol = merged_atom.setProperty("coordinates1", coordinates1).molecule() + + # Update the velocities. + if update_velocity: + velocities0 = squashed_atom0._sire_object.property("velocity") + velocities1 = squashed_atom1._sire_object.property("velocity") + siremol = merged_atom.setProperty("velocity0", velocities0).molecule() + siremol = merged_atom.setProperty("velocity1", velocities1).molecule() + + return _Molecule(siremol.commit()) + + +def _squashed_molecule_mapping(system, is_lambda1=False): + """ + This internal function returns a dictionary whose keys correspond to the + molecule index of the each molecule in the original merged system, and + whose values contain the corresponding index of the same molecule at the + specified endstate in the squashed system. + + Parameters + ---------- + + system : BioSimSpace._SireWrappers.System + The input merged system. + + is_lambda1 : bool + Whether to use the lambda=1 endstate. + + Returns + ------- + + mapping : dict(int, int) + The corresponding molecule mapping. + """ + # Get the perturbable molecules and their corresponding indices. + pertmol_idxs = [i for i, molecule in enumerate(system) if molecule.isPerturbable()] + + # Add them back at the end of the system. This is generally faster than keeping their order the same. + new_indices = list(range(system.nMolecules())) + for pertmol_idx in pertmol_idxs: + new_indices.remove(pertmol_idx) + + # Multi-residue molecules are squashed to one molecule with extra residues. + if system[pertmol_idx].nResidues() > 1: + new_indices.append(pertmol_idx) + # Since we have two squashed molecules, we pick the first one at lambda=0 and the second one at lambda = 1. + elif not is_lambda1: + new_indices.extend([pertmol_idx, None]) + else: + new_indices.extend([None, pertmol_idx]) + + # Create the old molecule index to new molecule index mapping. + mapping = {idx: i for i, idx in enumerate(new_indices) if idx is not None} + + return mapping + + +def _squashed_atom_mapping(system, is_lambda1=False, environment=True, **kwargs): + """ + This internal function returns a dictionary whose keys correspond to the atom + index of the each atom in the original merged system, and whose values + contain the corresponding index of the same atom at the specified endstate + in the squashed system. + + Parameters + ---------- + + system : BioSimSpace._SireWrappers.System + The input merged system. + + is_lambda1 : bool + Whether to use the lambda=1 endstate. + + environment : bool + Whether to include all environment atoms (i.e. ones that are not perturbed). + + kwargs : + Keyword arguments to pass to _squashed_atom_mapping_molecule(). + + Returns + ------- + + mapping : dict(int, int) + The corresponding atom mapping. + """ + if isinstance(system, _Molecule): + return _squashed_atom_mapping( + system.toSystem(), is_lambda1=is_lambda1, environment=environment, **kwargs + ) + + # Both mappings start from 0 and we add all offsets at the end. + atom_mapping = {} + atom_idx, squashed_atom_idx, squashed_atom_idx_perturbed = 0, 0, 0 + squashed_offset = sum(x.nAtoms() for x in system if not x.isPerturbable()) + for molecule in system: + if molecule.isPerturbable(): + residue_atom_mapping, n_squashed_atoms = _squashed_atom_mapping_molecule( + molecule, + offset_merged=atom_idx, + offset_squashed=squashed_offset + squashed_atom_idx_perturbed, + is_lambda1=is_lambda1, + environment=environment, + **kwargs, + ) + atom_mapping.update(residue_atom_mapping) + atom_idx += molecule.nAtoms() + squashed_atom_idx_perturbed += n_squashed_atoms + else: + atom_indices = _np.arange(atom_idx, atom_idx + molecule.nAtoms()) + squashed_atom_indices = _np.arange( + squashed_atom_idx, squashed_atom_idx + molecule.nAtoms() + ) + if environment: + atom_mapping.update(dict(zip(atom_indices, squashed_atom_indices))) + atom_idx += molecule.nAtoms() + squashed_atom_idx += molecule.nAtoms() + + # Convert from NumPy integers to Python integers. + return {int(k): int(v) for k, v in atom_mapping.items()} + + +def _squashed_atom_mapping_molecule( + molecule, + offset_merged=0, + offset_squashed=0, + is_lambda1=False, + environment=True, + common=True, + dummies=True, + explicit_dummies=False, +): + """ + This internal function returns a dictionary whose keys correspond to the atom + index of the each atom in the original merged molecule, and whose values + contain the corresponding index of the same atom at the specified endstate + in the squashed molecule at a particular offset. + + Parameters + ---------- + + molecule : BioSimSpace._SireWrappers.Molecule + The input merged molecule. + + offset_merged : int + The index at which to start the merged atom numbering. + + offset_squashed : int + The index at which to start the squashed atom numbering. + + is_lambda1 : bool + Whether to use the lambda=1 endstate. + + environment : bool + Whether to include all environment atoms (i.e. ones that are not perturbed). + + common : bool + Whether to include all common atoms (i.e. ones that are perturbed but are + not dummies in the endstate of interest). + + dummies : bool + Whether to include all dummy atoms (i.e. ones that are perturbed and are + dummies in the endstate of interest). + + explicit_dummies : bool + Whether to keep the dummy atoms explicit at the endstates or remove them. + + Returns + ------- + + mapping : dict(int, int) + The corresponding atom mapping. + + n_atoms : int + The number of squashed atoms that correspond to the squashed molecule. + """ + if not molecule.isPerturbable(): + if environment: + return { + offset_merged + i: offset_squashed + i for i in range(molecule.nAtoms()) + }, molecule.nAtoms() + else: + return {}, molecule.nAtoms() + + # Both mappings start from 0 and we add all offsets at the end. + mapping, mapping_lambda1 = {}, {} + atom_idx_merged, atom_idx_squashed, atom_idx_squashed_lambda1 = 0, 0, 0 + for residue in molecule.getResidues(): + if not (_is_perturbed(residue) or molecule.nResidues() == 1): + # The residue is not perturbed. + if common: + mapping.update( + { + atom_idx_merged + i: atom_idx_squashed + i + for i in range(residue.nAtoms()) + } + ) + atom_idx_merged += residue.nAtoms() + atom_idx_squashed += residue.nAtoms() + else: + # The residue is perturbed. + + # Determine the dummy and the non-dummy atoms. + types0 = [ + atom._sire_object.property("ambertype0") for atom in residue.getAtoms() + ] + types1 = [ + atom._sire_object.property("ambertype1") for atom in residue.getAtoms() + ] + + if explicit_dummies: + # If both endstates are dummies then we treat them as common core atoms + dummy0 = dummy1 = _np.asarray( + [("du" in x) or ("du" in y) for x, y in zip(types0, types1)] + ) + common0 = common1 = ~dummy0 + in_mol0 = in_mol1 = _np.asarray([True] * residue.nAtoms()) + else: + in_mol0 = _np.asarray(["du" not in x for x in types0]) + in_mol1 = _np.asarray(["du" not in x for x in types1]) + dummy0 = ~in_mol1 + dummy1 = ~in_mol0 + common0 = _np.logical_and(in_mol0, ~dummy0) + common1 = _np.logical_and(in_mol1, ~dummy1) + + ndummy0 = residue.nAtoms() - sum(in_mol1) + ndummy1 = residue.nAtoms() - sum(in_mol0) + ncommon = residue.nAtoms() - ndummy0 - ndummy1 + natoms0 = ncommon + ndummy0 + natoms1 = ncommon + ndummy1 + + # Determine the full mapping indices for the merged and squashed systems. + if not is_lambda1: + atom_indices = _np.arange( + atom_idx_merged, atom_idx_merged + residue.nAtoms() + )[in_mol0] + squashed_atom_indices = _np.arange( + atom_idx_squashed, atom_idx_squashed + natoms0 + ) + mapping_to_update = mapping + else: + atom_indices = _np.arange( + atom_idx_merged, atom_idx_merged + residue.nAtoms() + )[in_mol1] + squashed_atom_indices = _np.arange( + atom_idx_squashed_lambda1, atom_idx_squashed_lambda1 + natoms1 + ) + mapping_to_update = mapping_lambda1 + + # Determine which atoms to return. + in_mol_mask = in_mol1 if is_lambda1 else in_mol0 + common_mask = common1 if is_lambda1 else common0 + dummy_mask = dummy1 if is_lambda1 else dummy0 + update_mask = _np.asarray([False] * atom_indices.size) + + if common: + update_mask = _np.logical_or(update_mask, common_mask[in_mol_mask]) + if dummies: + update_mask = _np.logical_or(update_mask, dummy_mask[in_mol_mask]) + + # Finally update the relevant mapping + mapping_to_update.update( + dict(zip(atom_indices[update_mask], squashed_atom_indices[update_mask])) + ) + + # Increment the offsets and continue. + atom_idx_merged += residue.nAtoms() + atom_idx_squashed += natoms0 + atom_idx_squashed_lambda1 += natoms1 + + # Finally add the appropriate offsets + if explicit_dummies: + all_ndummy1 = 0 + else: + all_ndummy1 = sum( + "du" in x for x in molecule._sire_object.property("ambertype0").toVector() + ) + + offset_squashed_lambda1 = molecule.nAtoms() - all_ndummy1 + res = { + **{offset_merged + k: offset_squashed + v for k, v in mapping.items()}, + **{ + offset_merged + k: offset_squashed + offset_squashed_lambda1 + v + for k, v in mapping_lambda1.items() + }, + } + + return res, atom_idx_squashed + atom_idx_squashed_lambda1 + + +def _is_perturbed(residue): + """ + This determines whether a merged residue is actually perturbed. Note that + it is possible that this function returns false negatives. + + Parameters + ---------- + + residue : BioSimSpace._SireWrappers.Residue + The input residue. + + Returns + ------- + + res : bool + Whether the residue is perturbed. + """ + # If the elements are different, then we are definitely perturbing. + elem0 = [atom._sire_object.property("element0") for atom in residue.getAtoms()] + elem1 = [atom._sire_object.property("element1") for atom in residue.getAtoms()] + return elem0 != elem1 + + +def _amber_mask_from_indices(atom_idxs): + """ + Internal helper function to create an AMBER mask from a list of atom indices. + + Parameters + ---------- + + atom_idxs : [int] + A list of atom indices. + + Returns + ------- + + mask : str + The AMBER mask. + """ + # AMBER has a restriction on the number of characters in the restraint + # mask (not documented) so we can't just use comma-separated atom + # indices. Instead we loop through the indices and use hyphens to + # separate contiguous blocks of indices, e.g. 1-23,34-47,... + + if atom_idxs: + # AMBER masks are 1-indexed, while BioSimSpace indices are 0-indexed. + atom_idxs = [x + 1 for x in sorted(list(set(atom_idxs)))] + if not all(isinstance(x, int) for x in atom_idxs): + raise TypeError("'atom_idxs' must be a list of 'int' types.") + groups = [] + initial_idx = atom_idxs[0] + for prev_idx, curr_idx in _it.zip_longest(atom_idxs, atom_idxs[1:]): + if curr_idx != prev_idx + 1 or curr_idx is None: + if initial_idx == prev_idx: + groups += [str(initial_idx)] + else: + groups += [f"{initial_idx}-{prev_idx}"] + initial_idx = curr_idx + mask = "@" + ",".join(groups) + else: + mask = "" + + return mask diff --git a/python/BioSimSpace/FreeEnergy/_relative.py b/python/BioSimSpace/FreeEnergy/_relative.py index 8db00c5a8..ee1af9e7b 100644 --- a/python/BioSimSpace/FreeEnergy/_relative.py +++ b/python/BioSimSpace/FreeEnergy/_relative.py @@ -123,7 +123,7 @@ class Relative: """Class for configuring and running relative free-energy perturbation simulations.""" # Create a list of supported molecular dynamics engines. (For running simulations.) - _engines = ["GROMACS", "SOMD"] + _engines = ["AMBER", "GROMACS", "SOMD"] # Create a list of supported molecular dynamics engines. (For analysis.) _engines_analysis = ["AMBER", "GROMACS", "SOMD", "SOMD2"] @@ -140,6 +140,7 @@ def __init__( extra_options={}, extra_lines=[], property_map={}, + **kwargs, ): """ Constructor. @@ -157,6 +158,9 @@ def __init__( :class:`Protocol.FreeEnergyProduction ` The simulation protocol. + reference_system : :class:`System ` + A reference system to use for position restraints. + work_dir : str The working directory for the free-energy perturbation simulation. @@ -194,6 +198,10 @@ def __init__( A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their own naming scheme, e.g. { "charge" : "my-charge" } + + kwargs : dict + Additional keyword arguments to pass to the underlying Process + objects. """ # Validate the input. @@ -203,7 +211,7 @@ def __init__( "'system' must be of type 'BioSimSpace._SireWrappers.System'" ) else: - # Store a copy of solvated system. + # Store a copy of the system. self._system = system.copy() # Validate the user specified molecular dynamics engine. @@ -221,19 +229,12 @@ def __init__( "Supported engines are: %r." % ", ".join(self._engines) ) - # Make sure GROMACS is installed if GROMACS engine is selected. - if engine == "GROMACS": - if _gmx_exe is None: - raise _MissingSoftwareError( - "Cannot use GROMACS engine as GROMACS is not installed!" - ) - - # The system must have a perturbable molecule. - if system.nPerturbableMolecules() == 0: - raise ValueError( - "The system must contain a perturbable molecule! " - "Use the 'BioSimSpace.Align' package to map and merge molecules." - ) + # The system must have a perturbable molecule. + if system.nPerturbableMolecules() == 0: + raise ValueError( + "The system must contain a perturbable molecule! " + "Use the 'BioSimSpace.Align' package to map and merge molecules." + ) else: # Use SOMD as a default. @@ -322,6 +323,11 @@ def __init__( raise TypeError("'property_map' must be of type 'dict'") self._property_map = property_map + # Validate the kwargs. + if not isinstance(kwargs, dict): + raise TypeError("'kwargs' must be of type 'dict'.") + self._kwargs = kwargs + # Create fake instance methods for 'analyse', 'checkOverlap', # and 'difference'. These pass instance data through to the # staticmethod versions. @@ -2004,7 +2010,7 @@ def _initialise_runner(self, system): processes = [] # Convert to an appropriate water topology. - if self._engine == "SOMD": + if self._engine in ["AMBER", "SOMD"]: system._set_water_topology("AMBER", property_map=self._property_map) elif self._engine == "GROMACS": system._set_water_topology("GROMACS", property_map=self._property_map) @@ -2042,6 +2048,7 @@ def _initialise_runner(self, system): extra_options=self._extra_options, extra_lines=self._extra_lines, property_map=self._property_map, + **self._kwargs, ) if self._setup_only: del first_process @@ -2059,10 +2066,24 @@ def _initialise_runner(self, system): extra_options=self._extra_options, extra_lines=self._extra_lines, property_map=self._property_map, + **self._kwargs, ) if not self._setup_only: processes.append(first_process) + # AMBER. + elif self._engine == "AMBER": + first_process = _Process.Amber( + system, + self._protocol, + exe=self._exe, + work_dir=first_dir, + extra_options=self._extra_options, + extra_lines=self._extra_lines, + property_map=self._property_map, + **self._kwargs, + ) + # Loop over the rest of the lambda values. for x, lam in enumerate(lam_vals[1:]): # Name the directory. @@ -2164,6 +2185,7 @@ def _initialise_runner(self, system): process._std_err_file = new_dir + "/gromacs.err" process._gro_file = new_dir + "/gromacs.gro" process._top_file = new_dir + "/gromacs.top" + process._ref_file = new_dir + "/gromacs_ref.gro" process._traj_file = new_dir + "/gromacs.trr" process._config_file = new_dir + "/gromacs.mdp" process._tpr_file = new_dir + "/gromacs.tpr" @@ -2175,6 +2197,41 @@ def _initialise_runner(self, system): ] processes.append(process) + # AMBER. + elif self._engine == "AMBER": + new_config = [] + with open(new_dir + "/amber.cfg", "r") as f: + for line in f: + if "clambda" in line: + new_config.append(" clambda=%s,\n" % lam) + else: + new_config.append(line) + with open(new_dir + "/amber.cfg", "w") as f: + for line in new_config: + f.write(line) + + # Create a copy of the process and update the working + # directory. + if not self._setup_only: + process = _copy.copy(first_process) + process._system = first_process._system.copy() + process._protocol = self._protocol + process._work_dir = new_dir + process._std_out_file = new_dir + "/amber.out" + process._std_err_file = new_dir + "/amber.err" + process._rst_file = new_dir + "/amber.rst7" + process._top_file = new_dir + "/amber.prm7" + process._ref_file = new_dir + "/amber_ref.rst7" + process._traj_file = new_dir + "/amber.nc" + process._config_file = new_dir + "/amber.cfg" + process._nrg_file = new_dir + "/amber.nrg" + process._input_files = [ + process._config_file, + process._rst_file, + process._top_file, + ] + processes.append(process) + if not self._setup_only: # Initialise the process runner. All processes have already been nested # inside the working directory so no need to re-nest. diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index c5840d7f5..fb25ecc73 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -43,6 +43,7 @@ from sire.legacy import Mol as _SireMol from .. import _amber_home, _isVerbose +from ..Align._squash import _squash, _unsquash from .._Config import Amber as _AmberConfig from .._Exceptions import IncompatibleError as _IncompatibleError from .._Exceptions import MissingSoftwareError as _MissingSoftwareError @@ -70,6 +71,7 @@ def __init__( system, protocol, reference_system=None, + explicit_dummies=False, exe=None, name="amber", work_dir=None, @@ -95,6 +97,9 @@ def __init__( restraints. It is assumed that this system has the same topology as "system". If this is None, then "system" is used as a reference. + explicit_dummies : bool + Whether to keep dummy atoms explicit at alchemical end states, or remove them. + exe : str The full path to the AMBER executable. @@ -133,12 +138,6 @@ def __init__( property_map=property_map, ) - # Catch unsupported protocols. - if isinstance(protocol, _FreeEnergyMixin): - raise _IncompatibleError( - "Unsupported protocol: '%s'" % self._protocol.__class__.__name__ - ) - # Set the package name. self._package_name = "AMBER" @@ -168,9 +167,37 @@ def __init__( else: raise IOError("AMBER executable doesn't exist: '%s'" % exe) + if not isinstance(explicit_dummies, bool): + raise TypeError("'explicit_dummies' must be of type 'bool'") + self._explicit_dummies = explicit_dummies + # Initialise the energy dictionary and header. self._stdout_dict = _process._MultiDict() + # Initialise dictionaries to hold stdout records for all possible + # regions. For regular simulations there will be one, for free-energy + # simulations there can be up to four, i.e. one for each of the TI regions + # and one for the soft-core part of the system in each region, if present. + # The order of the dictionaries is: + # - TI region 1 + # - TI region 1 (soft-core part) + # - TI region 2 + # - TI region 2 (soft-core part) + self._stdout_dict = [ + _process._MultiDict(), + _process._MultiDict(), + _process._MultiDict(), + _process._MultiDict(), + ] + + # Initialise mappings between "universal" stdout keys, and the actual + # record key used for the different regions (and soft-core parts) from + # in the AMBER output. Ordering is the same as for the stdout_dicts above. + self._stdout_key = [{}, {}, {}, {}] + + # Flag for the current record region in the AMBER output file. + self._current_region = 0 + # Initialise log file parsing flags. self._has_results = False self._finished_results = False @@ -208,8 +235,15 @@ def _setup(self): # Convert the water model topology so that it matches the AMBER naming convention. system._set_water_topology("AMBER", property_map=self._property_map) - # Check for perturbable molecules and convert to the chosen end state. - system = self._checkPerturbable(system) + # Create the squashed system. + if isinstance(self._protocol, _FreeEnergyMixin): + system, self._mapping = _squash( + system, explicit_dummies=self._explicit_dummies + ) + self._squashed_system = system + else: + # Check for perturbable molecules and convert to the chosen end state. + system = self._checkPerturbable(system) # RST file (coordinates). try: @@ -312,7 +346,10 @@ def _generate_config(self): # Create the configuration. self.setConfig( amber_config.createConfig( - is_pmemd=is_pmemd, extra_options=extra_options, extra_lines=extra_lines + is_pmemd=is_pmemd, + explicit_dummies=self._explicit_dummies, + extra_options=extra_options, + extra_lines=extra_lines, ) ) @@ -340,7 +377,7 @@ def _generate_args(self): # Append a reference file if a position restraint is specified. if isinstance(self._protocol, _PositionRestraintMixin): if self._protocol.getRestraint() is not None: - self.setArg("-ref", self._ref_file) + self.setArg("-ref", "%s_ref.rst7" % self._name) # Append a trajectory file if this anything other than a minimisation. if not isinstance(self._protocol, _Protocol.Minimisation): @@ -447,23 +484,51 @@ def getSystem(self, block="AUTO"): # Create a copy of the existing system object. old_system = self._system.copy() - # Update the coordinates and velocities and return a mapping between - # the molecule indices in the two systems. - sire_system, mapping = _SireIO.updateCoordinatesAndVelocities( - old_system._sire_object, - new_system._sire_object, - self._mapping, - is_lambda1, - self._property_map, - self._property_map, - ) + if isinstance(self._protocol, _FreeEnergyMixin): + # Udpate the coordinates and velocities and return a mapping between + # the molecule indices in the two systems. + mapping = { + _SireMol.MolIdx(x): _SireMol.MolIdx(x) + for x in range(0, self._squashed_system.nMolecules()) + } + ( + self._squashed_system._sire_object, + _, + ) = _SireIO.updateCoordinatesAndVelocities( + self._squashed_system._sire_object, + new_system._sire_object, + mapping, + is_lambda1, + self._property_map, + self._property_map, + ) - # Update the underlying Sire object. - old_system._sire_object = sire_system + # Update the unsquashed system based on the updated squashed system. + old_system = _unsquash( + old_system, + self._squashed_system, + self._mapping, + explicit_dummies=self._explicit_dummies, + ) - # Store the mapping between the MolIdx in both systems so we don't - # need to recompute it next time. - self._mapping = mapping + else: + # Update the coordinates and velocities and return a mapping between + # the molecule indices in the two systems. + sire_system, mapping = _SireIO.updateCoordinatesAndVelocities( + old_system._sire_object, + new_system._sire_object, + self._mapping, + is_lambda1, + self._property_map, + self._property_map, + ) + + # Update the underlying Sire object. + old_system._sire_object = sire_system + + # Store the mapping between the MolIdx in both systems so we don't + # need to recompute it next time. + self._mapping = mapping # Update the box information in the original system. if "space" in new_system._sire_object.propertyKeys(): @@ -605,7 +670,65 @@ def getFrame(self, index): except: return None - def getRecord(self, key, time_series=False, unit=None, block="AUTO"): + def getRecordKey(self, record, region=0, soft_core=False): + """ + Parameters + ---------- + + record : str + The record used in the AMBER standard output, e.g. 'TEMP(K)'. + Please consult the current AMBER manual for details: + https://ambermd.org/Manuals.php + + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + + Returns + ------- + + key : str + The universal record key that can be used with getRecord. + """ + + # Validate the record string. + if not isinstance(record, str): + raise TypeError("'record' must be of type 'str'") + + # Validate the region. + if not isinstance(region, int): + raise TypeError("'region' must be of type 'int'") + else: + if region < 0 or region > 1: + raise ValueError("'region' must be in range [0, 1]") + + # Validate the soft-core flag. + if not isinstance(soft_core, bool): + raise TypeError("'soft_core' must be of type 'bool'.") + + # Convert to the full index. + idx = 2 * region + int(soft_core) + + # Strip whitespace from the beginning and end of the record and convert + # to upper case. + cleaned_record = record.strip().upper() + + # Make sure the record exists in the key mapping. + if not cleaned_record in self._stdout_key[idx].values(): + raise ValueError(f"No key found for record '{record}'") + + return list(self._stdout_key[idx].keys())[ + list(self._stdout_key[idx].values()).index(cleaned_record) + ] + + def getRecord( + self, key, time_series=False, unit=None, region=0, soft_core=False, block="AUTO" + ): """ Get a record from the stdout dictionary. @@ -613,7 +736,10 @@ def getRecord(self, key, time_series=False, unit=None, block="AUTO"): ---------- key : str - The record key. + A universal record key based on the key used in the AMBER standard + output. Use 'getRecordKey(record)` to generate the key. The records + are those used in the AMBER standard output, e.g. 'TEMP(K)'. Please + consult the current AMBER manual for details: https://ambermd.org/Manuals.php time_series : bool Whether to return a list of time series records. @@ -621,6 +747,15 @@ def getRecord(self, key, time_series=False, unit=None, block="AUTO"): unit : :class:`Unit ` The unit to convert the record to. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -642,10 +777,16 @@ def getRecord(self, key, time_series=False, unit=None, block="AUTO"): _warnings.warn("The process exited with an error!") return self._get_stdout_record( - key.strip().upper(), time_series=time_series, unit=unit + key.strip().upper(), + time_series=time_series, + unit=unit, + region=region, + soft_core=soft_core, ) - def getCurrentRecord(self, key, time_series=False, unit=None): + def getCurrentRecord( + self, key, time_series=False, unit=None, region=0, soft_core=False + ): """ Get a current record from the stdout dictionary. @@ -653,7 +794,10 @@ def getCurrentRecord(self, key, time_series=False, unit=None): ---------- key : str - The record key. + A universal record key based on the key used in the AMBER standard + output. Use 'getRecordKey(record)` to generate the key. The records + are those used in the AMBER standard output, e.g. 'TEMP(K)'. Please + consult the current AMBER manual for details: https://ambermd.org/Manuals.php time_series : bool Whether to return a list of time series records. @@ -661,6 +805,15 @@ def getCurrentRecord(self, key, time_series=False, unit=None): unit : :class:`Unit ` The unit to convert the record to. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- @@ -673,16 +826,29 @@ def getCurrentRecord(self, key, time_series=False, unit=None): _warnings.warn("The process exited with an error!") return self._get_stdout_record( - key.strip().upper(), time_series=time_series, unit=unit + key.strip().upper(), + time_series=time_series, + unit=unit, + region=region, + soft_core=soft_core, ) - def getRecords(self, block="AUTO"): + def getRecords(self, region=0, soft_core=False, block="AUTO"): """ Return the dictionary of stdout time-series records. Parameters ---------- + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -693,6 +859,20 @@ def getRecords(self, block="AUTO"): The dictionary of time-series records. """ + # Validate the region. + if not isinstance(region, int): + raise TypeError("'region' must be of type 'int'") + else: + if region < 0 or region > 1: + raise ValueError("'region' must be in range [0, 1]") + + # Validate the soft-core flag. + if not isinstance(soft_core, bool): + raise TypeError("'soft_core' must be of type 'bool'.") + + # Convert to the full index, region + soft_core. + idx = 2 * region + int(soft_core) + # Wait for the process to finish. if block is True: self.wait() @@ -704,21 +884,34 @@ def getRecords(self, block="AUTO"): _warnings.warn("The process exited with an error!") self.stdout(0) - return self._stdout_dict.copy() - def getCurrentRecords(self): + return self._stdout_dict[idx].copy() + + def getCurrentRecords(self, region=0, soft_core=False): """ Return the current dictionary of stdout time-series records. + Parameters + ---------- + + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- records : :class:`MultiDict ` The dictionary of time-series records. """ - return self.getRecords(block=False) + return self.getRecords(region=region, soft_core=soft_core, block=False) - def getTime(self, time_series=False, block="AUTO"): + def getTime(self, time_series=False, region=0, soft_core=False, block="AUTO"): """ Get the simulation time. @@ -728,6 +921,15 @@ def getTime(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -743,7 +945,14 @@ def getTime(self, time_series=False, block="AUTO"): return None # Get the list of time steps. - time_steps = self.getRecord("TIME(PS)", time_series=time_series, block=block) + time_steps = self.getRecord( + "TIME(PS)", + time_series=time_series, + unit=None, + region=region, + soft_core=soft_core, + block=block, + ) # Convert from picoseconds to nanoseconds. if time_steps is not None: @@ -754,7 +963,7 @@ def getTime(self, time_series=False, block="AUTO"): else: return (time_steps * _Units.Time.picosecond)._to_default_unit() - def getCurrentTime(self, time_series=False): + def getCurrentTime(self, time_series=False, region=0, soft_core=False): """ Get the current simulation time. @@ -764,15 +973,26 @@ def getCurrentTime(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- time : :class:`Time ` The current simulation time in nanoseconds. """ - return self.getTime(time_series, block=False) + return self.getTime( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getStep(self, time_series=False, block="AUTO"): + def getStep(self, time_series=False, region=0, soft_core=False, block="AUTO"): """ Get the number of integration steps. @@ -782,6 +1002,15 @@ def getStep(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -791,9 +1020,16 @@ def getStep(self, time_series=False, block="AUTO"): step : int The current number of integration steps. """ - return self.getRecord("NSTEP", time_series=time_series, block=block) + return self.getRecord( + "NSTEP", + time_series=time_series, + unit=None, + region=region, + soft_core=soft_core, + block=block, + ) - def getCurrentStep(self, time_series=False): + def getCurrentStep(self, time_series=False, region=0, soft_core=False): """ Get the current number of integration steps. @@ -803,15 +1039,26 @@ def getCurrentStep(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- step : int The current number of integration steps. """ - return self.getStep(time_series, block=False) + return self.getStep( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getBondEnergy(self, time_series=False, block="AUTO"): + def getBondEnergy(self, time_series=False, region=0, soft_core=False, block="AUTO"): """ Get the bond energy. @@ -821,6 +1068,10 @@ def getBondEnergy(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -834,10 +1085,12 @@ def getBondEnergy(self, time_series=False, block="AUTO"): "BOND", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentBondEnergy(self, time_series=False): + def getCurrentBondEnergy(self, time_series=False, region=0, soft_core=False): """ Get the current bond energy. @@ -847,15 +1100,28 @@ def getCurrentBondEnergy(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The bond energy. """ - return self.getBondEnergy(time_series, block=False) + return self.getBondEnergy( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getAngleEnergy(self, time_series=False, block="AUTO"): + def getAngleEnergy( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the angle energy. @@ -865,6 +1131,15 @@ def getAngleEnergy(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -878,10 +1153,12 @@ def getAngleEnergy(self, time_series=False, block="AUTO"): "ANGLE", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentAngleEnergy(self, time_series=False): + def getCurrentAngleEnergy(self, time_series=False, region=0, soft_core=False): """ Get the current angle energy. @@ -891,15 +1168,28 @@ def getCurrentAngleEnergy(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The angle energy. """ - return self.getAngleEnergy(time_series, block=False) + return self.getAngleEnergy( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getDihedralEnergy(self, time_series=False, block="AUTO"): + def getDihedralEnergy( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the total dihedral energy (proper + improper). @@ -909,6 +1199,15 @@ def getDihedralEnergy(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -922,10 +1221,12 @@ def getDihedralEnergy(self, time_series=False, block="AUTO"): "DIHED", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentDihedralEnergy(self, time_series=False): + def getCurrentDihedralEnergy(self, time_series=False, region=0, soft_core=False): """ Get the current total dihedral energy (proper + improper). @@ -935,15 +1236,28 @@ def getCurrentDihedralEnergy(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The total dihedral energy. """ - return self.getDihedralEnergy(time_series, block=False) + return self.getDihedralEnergy( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getElectrostaticEnergy(self, time_series=False, block="AUTO"): + def getElectrostaticEnergy( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the electrostatic energy. @@ -953,6 +1267,15 @@ def getElectrostaticEnergy(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -963,13 +1286,17 @@ def getElectrostaticEnergy(self, time_series=False, block="AUTO"): The electrostatic energy. """ return self.getRecord( - "EELEC", + "EEL", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentElectrostaticEnergy(self, time_series=False): + def getCurrentElectrostaticEnergy( + self, time_series=False, region=0, soft_core=False + ): """ Get the current dihedral energy. @@ -979,15 +1306,28 @@ def getCurrentElectrostaticEnergy(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The electrostatic energy. """ - return self.getElectrostaticEnergy(time_series, block=False) + return self.getElectrostaticEnergy( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getElectrostaticEnergy14(self, time_series=False, block="AUTO"): + def getElectrostaticEnergy14( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the electrostatic energy between atoms 1 and 4. @@ -997,6 +1337,15 @@ def getElectrostaticEnergy14(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1007,13 +1356,17 @@ def getElectrostaticEnergy14(self, time_series=False, block="AUTO"): The electrostatic energy between atoms 1 and 4. """ return self.getRecord( - "1-4 EEL", + "14EEL", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentElectrostaticEnergy14(self, time_series=False): + def getCurrentElectrostaticEnergy14( + self, time_series=False, region=0, soft_core=False + ): """ Get the current electrostatic energy between atoms 1 and 4. @@ -1023,15 +1376,28 @@ def getCurrentElectrostaticEnergy14(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The electrostatic energy between atoms 1 and 4. """ - return self.getElectrostaticEnergy14(time_series, block=False) + return self.getElectrostaticEnergy14( + time_series=time_series, region=region, soft_core=False, block=False + ) - def getVanDerWaalsEnergy(self, time_series=False, block="AUTO"): + def getVanDerWaalsEnergy( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the Van der Vaals energy. @@ -1041,6 +1407,15 @@ def getVanDerWaalsEnergy(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1051,13 +1426,15 @@ def getVanDerWaalsEnergy(self, time_series=False, block="AUTO"): The Van der Vaals energy. """ return self.getRecord( - "VDWAALS", + "VDW", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentVanDerWaalsEnergy(self, time_series=False): + def getCurrentVanDerWaalsEnergy(self, time_series=False, region=0, soft_core=False): """ Get the current Van der Vaals energy. @@ -1067,15 +1444,98 @@ def getCurrentVanDerWaalsEnergy(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The Van der Vaals energy. """ - return self.getVanDerWaalsEnergy(time_series, block=False) + return self.getVanDerWaalsEnergy( + time_series=time_series, block=False, region=region, soft_core=soft_core + ) + + def getVanDerWaalsEnergy14( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): + """ + Get the Van der Vaals energy between atoms 1 and 4. + + Parameters + ---------- + + time_series : bool + Whether to return a list of time series records. + + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + + block : bool + Whether to block until the process has finished running. + + Returns + ------- + + energy : :class:`Energy ` + The Van der Vaals energy between atoms 1 and 4. + """ + return self.getRecord( + "14VDW", + time_series=time_series, + unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, + block=block, + ) - def getHydrogenBondEnergy(self, time_series=False, block="AUTO"): + def getCurrentVanDerWaalsEnergy14( + self, time_series=False, region=0, soft_core=False + ): + """ + Get the current Van der Vaals energy between atoms 1 and 4. + + Parameters + ---------- + + time_series : bool + Whether to return a list of time series records. + + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + + Returns + ------- + + energy : :class:`Energy ` + The Van der Vaals energy between atoms 1 and 4. + """ + return self.getVanDerWaalsEnergy( + time_series=time_series, block=False, region=region, soft_core=soft_core + ) + + def getHydrogenBondEnergy( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the hydrogen bond energy. @@ -1085,6 +1545,15 @@ def getHydrogenBondEnergy(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1098,10 +1567,14 @@ def getHydrogenBondEnergy(self, time_series=False, block="AUTO"): "EHBOND", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentHydrogenBondEnergy(self, time_series=False): + def getCurrentHydrogenBondEnergy( + self, time_series=False, region=0, soft_core=False + ): """ Get the current hydrogen bond energy. @@ -1111,15 +1584,28 @@ def getCurrentHydrogenBondEnergy(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The hydrogen bond energy. """ - return self.getHydrogenBondEnergy(time_series, block=False) + return self.getHydrogenBondEnergy( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getRestraintEnergy(self, time_series=False, block="AUTO"): + def getRestraintEnergy( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the restraint energy. @@ -1129,6 +1615,15 @@ def getRestraintEnergy(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1142,10 +1637,12 @@ def getRestraintEnergy(self, time_series=False, block="AUTO"): "RESTRAINT", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentRestraintEnergy(self, time_series=False): + def getCurrentRestraintEnergy(self, time_series=False, region=0, soft_core=False): """ Get the current restraint energy. @@ -1155,6 +1652,15 @@ def getCurrentRestraintEnergy(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1164,9 +1670,13 @@ def getCurrentRestraintEnergy(self, time_series=False): energy : :class:`Energy ` The restraint energy. """ - return self.getRestraintEnergy(time_series, block=False) + return self.getRestraintEnergy( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getPotentialEnergy(self, time_series=False, block="AUTO"): + def getPotentialEnergy( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the potential energy. @@ -1176,6 +1686,15 @@ def getPotentialEnergy(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1189,10 +1708,12 @@ def getPotentialEnergy(self, time_series=False, block="AUTO"): "EPTOT", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentPotentialEnergy(self, time_series=False): + def getCurrentPotentialEnergy(self, time_series=False, region=0, soft_core=False): """ Get the current potential energy. @@ -1202,15 +1723,28 @@ def getCurrentPotentialEnergy(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The potential energy. """ - return self.getPotentialEnergy(time_series, block=False) + return self.getPotentialEnergy( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getKineticEnergy(self, time_series=False, block="AUTO"): + def getKineticEnergy( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the kinetic energy. @@ -1220,6 +1754,15 @@ def getKineticEnergy(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1233,10 +1776,12 @@ def getKineticEnergy(self, time_series=False, block="AUTO"): "EKTOT", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentKineticEnergy(self, time_series=False): + def getCurrentKineticEnergy(self, time_series=False, region=0, soft_core=False): """ Get the current kinetic energy. @@ -1246,15 +1791,28 @@ def getCurrentKineticEnergy(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The kinetic energy. """ - return self.getKineticEnergy(time_series, block=False) + return self.getKineticEnergy( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getNonBondedEnergy14(self, time_series=False, block="AUTO"): + def getNonBondedEnergy14( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the non-bonded energy between atoms 1 and 4. @@ -1264,6 +1822,15 @@ def getNonBondedEnergy14(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1274,13 +1841,15 @@ def getNonBondedEnergy14(self, time_series=False, block="AUTO"): The non-bonded energy between atoms 1 and 4. """ return self.getRecord( - "1-4 NB", + "14NB", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentNonBondedEnergy14(self, time_series=False): + def getCurrentNonBondedEnergy14(self, time_series=False, region=0, soft_core=False): """ Get the current non-bonded energy between atoms 1 and 4. @@ -1290,15 +1859,28 @@ def getCurrentNonBondedEnergy14(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The non-bonded energy between atoms 1 and 4. """ - return self.getNonBondedEnergy14(time_series, block=False) + return self.getNonBondedEnergy14( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getTotalEnergy(self, time_series=False, block="AUTO"): + def getTotalEnergy( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the total energy. @@ -1308,6 +1890,15 @@ def getTotalEnergy(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1317,11 +1908,27 @@ def getTotalEnergy(self, time_series=False, block="AUTO"): energy : :class:`Energy ` The total energy. """ - if isinstance(self._protocol, _Protocol.Minimisation): + + if not isinstance(region, int): + raise TypeError("'region' must be of type 'int'") + else: + if region < 0 or region > 1: + raise ValueError("'region' must be in range [0, 1]") + + # Validate the soft-core flag. + if not isinstance(soft_core, bool): + raise TypeError("'soft_core' must be of type 'bool'.") + + # Convert to the full index, region + soft_core. + idx = 2 * region + int(soft_core) + + if isinstance(self._protocol, _Protocol.Minimisation) and not soft_core: return self.getRecord( "ENERGY", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) else: @@ -1329,10 +1936,12 @@ def getTotalEnergy(self, time_series=False, block="AUTO"): "ETOT", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentTotalEnergy(self, time_series=False): + def getCurrentTotalEnergy(self, time_series=False, region=0, soft_core=False): """ Get the current total energy. @@ -1342,15 +1951,28 @@ def getCurrentTotalEnergy(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The total energy. """ - return self.getTotalEnergy(time_series, block=False) + return self.getTotalEnergy( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getCentreOfMassKineticEnergy(self, time_series=False, block="AUTO"): + def getCentreOfMassKineticEnergy( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the kinetic energy of the centre of mass in translation. @@ -1360,6 +1982,15 @@ def getCentreOfMassKineticEnergy(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1373,10 +2004,14 @@ def getCentreOfMassKineticEnergy(self, time_series=False, block="AUTO"): "EKCMT", time_series=time_series, unit=_Units.Energy.kcal_per_mol, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentCentreOfMassKineticEnergy(self, time_series=False): + def getCurrentCentreOfMassKineticEnergy( + self, time_series=False, region=0, soft_core=False + ): """ Get the current kinetic energy of the centre of mass in translation. @@ -1386,15 +2021,26 @@ def getCurrentCentreOfMassKineticEnergy(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- energy : :class:`Energy ` The centre of mass kinetic energy. """ - return self.getCentreOfMassKineticEnergy(time_series, block=False) + return self.getCentreOfMassKineticEnergy( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getVirial(self, time_series=False, block="AUTO"): + def getVirial(self, time_series=False, region=0, soft_core=False, block="AUTO"): """ Get the virial. @@ -1404,6 +2050,15 @@ def getVirial(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1413,9 +2068,15 @@ def getVirial(self, time_series=False, block="AUTO"): virial : float The virial. """ - return self.getRecord("VIRIAL", time_series=time_series, block=block) + return self.getRecord( + "VIRIAL", + time_series=time_series, + region=region, + soft_core=soft_core, + block=block, + ) - def getCurrentVirial(self, time_series=False): + def getCurrentVirial(self, time_series=False, region=0, soft_core=False): """ Get the current virial. @@ -1425,15 +2086,28 @@ def getCurrentVirial(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- virial : float The virial. """ - return self.getVirial(time_series, block=False) + return self.getVirial( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getTemperature(self, time_series=False, block="AUTO"): + def getTemperature( + self, time_series=False, region=0, soft_core=False, block="AUTO" + ): """ Get the temperature. @@ -1443,6 +2117,15 @@ def getTemperature(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1456,10 +2139,12 @@ def getTemperature(self, time_series=False, block="AUTO"): "TEMP(K)", time_series=time_series, unit=_Units.Temperature.kelvin, + region=region, + soft_core=soft_core, block=block, ) - def getCurrentTemperature(self, time_series=False): + def getCurrentTemperature(self, time_series=False, region=0, soft_core=False): """ Get the current temperature. @@ -1469,15 +2154,26 @@ def getCurrentTemperature(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- temperature : :class:`Temperature ` The temperature. """ - return self.getTemperature(time_series, block=False) + return self.getTemperature( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getPressure(self, time_series=False, block="AUTO"): + def getPressure(self, time_series=False, region=0, soft_core=False, block="AUTO"): """ Get the pressure. @@ -1487,6 +2183,15 @@ def getPressure(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1497,10 +2202,15 @@ def getPressure(self, time_series=False, block="AUTO"): The pressure. """ return self.getRecord( - "PRESS", time_series=time_series, unit=_Units.Pressure.bar, block=block + "PRESS", + time_series=time_series, + unit=_Units.Pressure.bar, + region=region, + soft_core=soft_core, + block=block, ) - def getCurrentPressure(self, time_series=False): + def getCurrentPressure(self, time_series=False, region=0, soft_core=False): """ Get the current pressure. @@ -1510,15 +2220,26 @@ def getCurrentPressure(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- pressure : :class:`Pressure ` The pressure. """ - return self.getPressure(time_series, block=False) + return self.getPressure( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getVolume(self, time_series=False, block="AUTO"): + def getVolume(self, time_series=False, region=0, soft_core=False, block="AUTO"): """ Get the volume. @@ -1528,6 +2249,15 @@ def getVolume(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1538,10 +2268,15 @@ def getVolume(self, time_series=False, block="AUTO"): The volume. """ return self.getRecord( - "VOLUME", time_series=time_series, unit=_Units.Volume.angstrom3, block=block + "VOLUME", + time_series=time_series, + unit=_Units.Volume.angstrom3, + region=region, + soft_core=soft_core, + block=block, ) - def getCurrentVolume(self, time_series=False): + def getCurrentVolume(self, time_series=False, region=0, soft_core=False): """ Get the current volume. @@ -1551,15 +2286,26 @@ def getCurrentVolume(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- volume : :class:`Volume ` The volume. """ - return self.getVolume(time_series, block=False) + return self.getVolume( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) - def getDensity(self, time_series=False, block="AUTO"): + def getDensity(self, time_series=False, region=0, soft_core=False, block="AUTO"): """ Get the density. @@ -1569,6 +2315,15 @@ def getDensity(self, time_series=False, block="AUTO"): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + block : bool Whether to block until the process has finished running. @@ -1578,9 +2333,15 @@ def getDensity(self, time_series=False, block="AUTO"): density : float The density. """ - return self.getRecord("DENSITY", time_series=time_series, block=block) + return self.getRecord( + "DENSITY", + time_series=time_series, + region=region, + soft_core=soft_core, + block=block, + ) - def getCurrentDensity(self, time_series=False): + def getCurrentDensity(self, time_series=False, region=0, soft_core=False): """ Get the current density. @@ -1590,13 +2351,89 @@ def getCurrentDensity(self, time_series=False): time_series : bool Whether to return a list of time series records. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- density : float The density. """ - return self.getDensity(time_series, block=False) + return self.getDensity( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) + + def getDVDL(self, time_series=False, region=0, soft_core=False, block="AUTO"): + """ + Get the gradient of the total energy with respect to lambda. + + Parameters + ---------- + + time_series : bool + Whether to return a list of time series records. + + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + + block : bool + Whether to block until the process has finished running. + + Returns + ------- + + dv_dl : float + The gradient of the total energy with respect to lambda. + """ + return self.getRecord( + "DVDL", + time_series=time_series, + region=region, + soft_core=soft_core, + block=block, + ) + + def getCurrentDVDL(self, time_series=False, region=0, soft_core=False): + """ + Get the current gradient of the total energy with respect to lambda. + + Parameters + ---------- + + time_series : bool + Whether to return a list of time series records. + + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + + Returns + ------- + + dv_dl : float + The current gradient of the total energy with respect to lambda. + """ + return self.getDVDL( + time_series=time_series, region=region, soft_core=soft_core, block=False + ) def stdout(self, n=10): """ @@ -1621,15 +2458,39 @@ def stdout(self, n=10): self._stdout.append(line.rstrip()) line = line.strip() + # Swap dictionary based on the protocol and the degre of freedom to + # which the next block of records correspond. + if isinstance(self._protocol, _FreeEnergyMixin): + if "TI region 1" in line: + self._current_region = 0 + elif "TI region 2" in line: + self._current_region = 2 + elif "Softcore part" in line and self._current_region == 0: + self._current_region = 1 + elif "Softcore part" in line and self._current_region == 2: + self._current_region = 3 + elif "Detailed TI info" in line: + # This flags that we should skip records until the start of + # the next set for the first TI region. + self._current_region = 4 + # Default stdout dictionary. + else: + self._current_region = 0 + + # Continue if we are ignoring this record block. + if self._current_region == 4: + continue + + stdout_dict = self._stdout_dict[self._current_region] + stdout_key = self._stdout_key[self._current_region] + # Skip empty lines and summary reports. - if ( - len(line) > 0 - and line[0] != "|" - and line[0] != "-" - and not line.startswith("EAMBER") - ): + if len(line) > 0 and line[0] != "|" and line[0] != "-": + # Skip EAMBER records. + if "EAMBER (non-restraint)" in line: + continue # Flag that we've started recording results. - if not self._has_results and line.startswith("NSTEP"): + elif not self._has_results and line.startswith("NSTEP"): self._has_results = True self._finished_results = False # Flag that we've finished recording results. @@ -1638,7 +2499,7 @@ def stdout(self, n=10): # Parse the results. if self._has_results and not self._finished_results: - # The output format is different for minimisation protocols. + # The first line of output has different formatting for minimisation protocols. if isinstance(self._protocol, _Protocol.Minimisation): # No equals sign in the line. if "NSTEP" in line and "=" not in line: @@ -1658,32 +2519,58 @@ def stdout(self, n=10): # The file hasn't been updated. if ( - "NSTEP" in self._stdout_dict - and data[0] == self._stdout_dict["NSTEP"][-1] + "NSTEP" in stdout_dict + and data[0] == stdout_dict["NSTEP"][-1] ): self._finished_results = True continue # Add the timestep and energy records to the dictionary. - self._stdout_dict["NSTEP"] = data[0] - self._stdout_dict["ENERGY"] = data[1] + stdout_dict["NSTEP"] = data[0] + stdout_dict["ENERGY"] = data[1] + + # Add the keys to the mapping + stdout_key["NSTEP"] = "NSTEP" + stdout_key["ENERGY"] = "ENERGY" # Turn off the header flag now that the data has been recorded. self._is_header = False - # All other protocols have output that is formatted as RECORD = VALUE. + # All other records are formatted as RECORD = VALUE. # Use a regex search to split the line into record names and values. records = _re.findall( - r"(\d*\-*\d*\s*[A-Z]+\(*[A-Z]*\)*)\s*=\s*(\-*\d+\.?\d*)", + r"([SC_]*[EEL_]*[RES_]*[VDW_]*\d*\-*\d*\s*[A-Z/]+\(*[A-Z]*\)*)\s*=\s*(\-*\d+\.?\d*|\**)", line.upper(), ) # Append each record to the dictionary. for key, value in records: - # Strip whitespace from the record key. + # Strip whitespace from beginning and end. key = key.strip() - self._stdout_dict[key] = value + + # Format key so it can be re-used for records corresponding to + # different regions, which use different abbreviations. + universal_key = ( + key.replace("SC_", "") + .replace(" ", "") + .replace("-", "") + .replace("EELEC", "EEL") + .replace("VDWAALS", "VDW") + ) + + # Handle missing values, which will appear as asterisks, e.g. + # PRESS=******** + try: + tmp = float(value) + except: + value = None + + # Store the record using the original key. + stdout_dict[key] = value + + # Map the universal key to the original. + stdout_key[universal_key] = key # Get the current number of lines. num_lines = len(self._stdout) @@ -1705,7 +2592,9 @@ def kill(self): if not self._process is None and self._process.isRunning(): self._process.kill() - def _get_stdout_record(self, key, time_series=False, unit=None): + def _get_stdout_record( + self, key, time_series=False, unit=None, region=0, soft_core=False + ): """ Helper function to get a stdout record from the dictionary. @@ -1713,7 +2602,7 @@ def _get_stdout_record(self, key, time_series=False, unit=None): ---------- key : str - The record key. + The universal record key. time_series : bool Whether to return a time series of records. @@ -1721,6 +2610,15 @@ def _get_stdout_record(self, key, time_series=False, unit=None): unit : :class:`Type ` The unit to convert the record to. + region : int + The region to which the record corresponds. There will only be more + than one region for FreeEnergy protocols, where 1 indicates the second + TI region. + + soft_core : bool + Whether to get the record for the soft-core part of the system for the + chosen region. + Returns ------- @@ -1744,18 +2642,41 @@ def _get_stdout_record(self, key, time_series=False, unit=None): if not isinstance(unit, _Type): raise TypeError("'unit' must be of type 'BioSimSpace.Types'") + # Validate the region. + if not isinstance(region, int): + raise TypeError("'region' must be of type 'int'") + else: + if region < 0 or region > 1: + raise ValueError("'region' must be in range [0, 1]") + + # Validate the soft-core flag. + if not isinstance(soft_core, bool): + raise TypeError("'soft_core' must be of type 'bool'.") + + # Convert to the full index, region + soft_core. + idx = 2 * region + int(soft_core) + + # Extract the dictionary of stdout records for the specified region and soft-core flag. + stdout_dict = self._stdout_dict[idx] + + # Map the universal key to the original key used for this region. + try: + key = self._stdout_key[idx][key] + except: + return None + # Return the list of dictionary values. if time_series: try: if key == "NSTEP": - return [int(x) for x in self._stdout_dict[key]] + return [int(x) for x in stdout_dict[key]] else: if unit is None: - return [float(x) for x in self._stdout_dict[key]] + return [float(x) if x else None for x in stdout_dict[key]] else: return [ - (float(x) * unit)._to_default_unit() - for x in self._stdout_dict[key] + (float(x) * unit)._to_default_unit() if x else None + for x in stdout_dict[key] ] except KeyError: @@ -1765,14 +2686,20 @@ def _get_stdout_record(self, key, time_series=False, unit=None): else: try: if key == "NSTEP": - return int(self._stdout_dict[key][-1]) + return int(stdout_dict[key][-1]) else: if unit is None: - return float(self._stdout_dict[key][-1]) + try: + return float(stdout_dict[key][-1]) + except: + return None else: - return ( - float(self._stdout_dict[key][-1]) * unit - )._to_default_unit() + try: + return ( + float(stdout_dict[key][-1]) * unit + )._to_default_unit() + except: + return None except KeyError: return None diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py index 7c9f66309..257deef09 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py @@ -805,23 +805,51 @@ def getFrame(self, index): # Create a copy of the existing system object. old_system = self._system.copy() - # Update the coordinates and velocities and return a mapping between - # the molecule indices in the two systems. - sire_system, mapping = _SireIO.updateCoordinatesAndVelocities( - old_system._sire_object, - new_system._sire_object, - self._mapping, - is_lambda1, - self._property_map, - self._property_map, - ) + if isinstance(self._protocol, _Protocol._FreeEnergyMixin): + # Udpate the coordinates and velocities and return a mapping between + # the molecule indices in the two systems. + mapping = { + _SireMol.MolIdx(x): _SireMol.MolIdx(x) + for x in range(0, self._squashed_system.nMolecules()) + } + ( + self._squashed_system._sire_object, + _, + ) = _SireIO.updateCoordinatesAndVelocities( + self._squashed_system._sire_object, + new_system._sire_object, + mapping, + is_lambda1, + self._property_map, + self._property_map, + ) + + # Update the unsquashed system based on the updated squashed system. + old_system = _unsquash( + old_system, + self._squashed_system, + self._mapping, + explicit_dummies=self._explicit_dummies, + ) + + else: + # Update the coordinates and velocities and return a mapping between + # the molecule indices in the two systems. + sire_system, mapping = _SireIO.updateCoordinatesAndVelocities( + old_system._sire_object, + new_system._sire_object, + self._mapping, + is_lambda1, + self._property_map, + self._property_map, + ) - # Update the underlying Sire object. - old_system._sire_object = sire_system + # Update the underlying Sire object. + old_system._sire_object = sire_system - # Store the mapping between the MolIdx in both systems so we don't - # need to recompute it next time. - self._mapping = mapping + # Store the mapping between the MolIdx in both systems so we don't + # need to recompute it next time. + self._mapping = mapping # Update the box information in the original system. if "space" in new_system._sire_object.propertyKeys(): diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index a25a5109c..d92eb1145 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -31,7 +31,9 @@ from sire.legacy import Units as _SireUnits +from ..Align._squash import _amber_mask_from_indices, _squashed_atom_mapping from .. import Protocol as _Protocol +from ..Protocol._free_energy_mixin import _FreeEnergyMixin from ..Protocol._position_restraint_mixin import _PositionRestraintMixin from ._config import Config as _Config @@ -63,7 +65,12 @@ def __init__(self, system, protocol, property_map={}): super().__init__(system, protocol, property_map=property_map) def createConfig( - self, version=None, is_pmemd=False, extra_options={}, extra_lines=[] + self, + version=None, + is_pmemd=False, + explicit_dummies=False, + extra_options={}, + extra_lines=[], ): """ Create the list of configuration strings. @@ -74,6 +81,9 @@ def createConfig( is_pmemd : bool Whether the configuration is for a simulation using PMEMD. + explicit_dummies : bool + Whether to keep the dummy atoms explicit at the endstates or remove them. + extra_options : dict A dictionary containing extra options. Overrides the defaults generated by the protocol. @@ -96,6 +106,9 @@ def createConfig( if not isinstance(is_pmemd, bool): raise TypeError("'is_pmemd' must be of type 'bool'.") + if not isinstance(explicit_dummies, bool): + raise TypeError("'explicit_dummies' must be of type 'bool'.") + if not isinstance(extra_options, dict): raise TypeError("'extra_options' must be of type 'dict'.") else: @@ -134,6 +147,9 @@ def createConfig( # Only read coordinates from file. protocol_dict["ntx"] = 1 + # Initialise a null timestep. + timestep = None + # Minimisation. if isinstance(self._protocol, _Protocol.Minimisation): # Work out the number of steepest descent cycles. @@ -197,6 +213,19 @@ def createConfig( else: atom_idxs = restraint + # Convert to a squashed representation, if needed + if isinstance(self._protocol, _FreeEnergyMixin): + atom_mapping0 = _squashed_atom_mapping( + self.system, is_lambda1=False + ) + atom_mapping1 = _squashed_atom_mapping( + self._system, is_lambda1=True + ) + atom_idxs = sorted( + {atom_mapping0[x] for x in atom_idxs if x in atom_mapping0} + | {atom_mapping1[x] for x in atom_idxs if x in atom_mapping1} + ) + # Don't add restraints if there are no atoms to restrain. if len(atom_idxs) > 0: # Generate the restraint mask based on atom indices. @@ -303,6 +332,39 @@ def createConfig( # Final temperature. protocol_dict["temp0"] = f"{temp:.2f}" + # Free energies. + if isinstance(self._protocol, _FreeEnergyMixin): + # Free energy mode. + protocol_dict["icfe"] = 1 + # Use softcore potentials. + protocol_dict["ifsc"] = 1 + # Remove SHAKE constraints. + protocol_dict["ntf"] = 1 + + # Get the list of lambda values. + lambda_values = [f"{x:.5f}" for x in self._protocol.getLambdaValues()] + + # Number of states in the MBAR calculation. (Number of lambda values.) + protocol_dict["mbar_states"] = len(lambda_values) + + # Lambda values for the MBAR calculation. + protocol_dict["mbar_lambda"] = ", ".join(lambda_values) + + # Current lambda value. + protocol_dict["clambda"] = "{:.5f}".format(self._protocol.getLambda()) + + if isinstance(self._protocol, _Protocol.Production): + # Calculate MBAR energies. + protocol_dict["ifmbar"] = 1 + # Output dVdl + protocol_dict["logdvdl"] = 1 + + # Atom masks. + protocol_dict = { + **protocol_dict, + **self._generate_amber_fep_masks(timestep), + } + # Put everything together in a line-by-line format. total_dict = {**protocol_dict, **extra_options} dict_lines = [self._protocol.__class__.__name__, "&cntrl"] @@ -389,3 +451,70 @@ def _create_restraint_mask(self, atom_idxs): restraint_mask += f",{idx+1}" return restraint_mask + + def _generate_amber_fep_masks(self, timestep, explicit_dummies=False): + """ + Internal helper function which generates timasks and scmasks based + on the system. + + Parameters + ---------- + + timestep : [float] + The timestep in ps for the FEP perturbation. Generates a different + mask based on this. + + explicit_dummies : bool + Whether to keep the dummy atoms explicit at the endstates or remove them. + + Returns + ------- + + option_dict : dict + A dictionary of AMBER-compatible options. + """ + # Get the merged to squashed atom mapping of the whole system for both endpoints. + kwargs = dict(environment=False, explicit_dummies=explicit_dummies) + mcs_mapping0 = _squashed_atom_mapping( + self._system, is_lambda1=False, common=True, dummies=False, **kwargs + ) + mcs_mapping1 = _squashed_atom_mapping( + self._system, is_lambda1=True, common=True, dummies=False, **kwargs + ) + dummy_mapping0 = _squashed_atom_mapping( + self._system, is_lambda1=False, common=False, dummies=True, **kwargs + ) + dummy_mapping1 = _squashed_atom_mapping( + self._system, is_lambda1=True, common=False, dummies=True, **kwargs + ) + + # Generate the TI and dummy masks. + mcs0_indices, mcs1_indices, dummy0_indices, dummy1_indices = [], [], [], [] + for i in range(self._system.nAtoms()): + if i in dummy_mapping0: + dummy0_indices.append(dummy_mapping0[i]) + if i in dummy_mapping1: + dummy1_indices.append(dummy_mapping1[i]) + if i in mcs_mapping0: + mcs0_indices.append(mcs_mapping0[i]) + if i in mcs_mapping1: + mcs1_indices.append(mcs_mapping1[i]) + ti0_indices = mcs0_indices + dummy0_indices + ti1_indices = mcs1_indices + dummy1_indices + + # SHAKE should be used for timestep > 2 fs. + if timestep is not None and timestep >= 0.002: + no_shake_mask = "" + else: + no_shake_mask = _amber_mask_from_indices(ti0_indices + ti1_indices) + + # Create an option dict with amber masks generated from the above indices. + option_dict = { + "timask1": f'"{_amber_mask_from_indices(ti0_indices)}"', + "timask2": f'"{_amber_mask_from_indices(ti1_indices)}"', + "scmask1": f'"{_amber_mask_from_indices(dummy0_indices)}"', + "scmask2": f'"{_amber_mask_from_indices(dummy1_indices)}"', + "noshakemask": f'"{no_shake_mask}"', + } + + return option_dict diff --git a/python/BioSimSpace/_SireWrappers/_molecule.py b/python/BioSimSpace/_SireWrappers/_molecule.py index 02b662c14..953bb76ea 100644 --- a/python/BioSimSpace/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/_SireWrappers/_molecule.py @@ -1593,7 +1593,11 @@ def _fixCharge(self, property_map={}): self._sire_object = edit_mol.commit() def _toRegularMolecule( - self, property_map={}, is_lambda1=False, convert_amber_dummies=False + self, + property_map={}, + is_lambda1=False, + convert_amber_dummies=False, + generate_intrascale=False, ): """ Internal function to convert a merged molecule to a regular molecule. @@ -1615,6 +1619,9 @@ def _toRegularMolecule( non-FEP simulations. This will replace the "du" ambertype and "Xx" element with the properties from the other end state. + generate_intrascale : bool + Whether to regenerate the intrascale matrix. + Returns ------- @@ -1628,6 +1635,9 @@ def _toRegularMolecule( if not isinstance(convert_amber_dummies, bool): raise TypeError("'convert_amber_dummies' must be of type 'bool'") + if not isinstance(generate_intrascale, bool): + raise TypeError("'generate_intrascale' must be of type 'bool'") + if is_lambda1: lam = "1" else: @@ -1710,6 +1720,17 @@ def _toRegularMolecule( mol = mol.removeProperty("element0").molecule() mol = mol.removeProperty("element1").molecule() + if generate_intrascale: + # First we regenerate the connectivity based on the bonds. + conn = _SireMol.Connectivity(mol.info()).edit() + for bond in mol.property("bond").potentials(): + conn.connect(bond.atom0(), bond.atom1()) + mol.setProperty("connectivity", conn.commit()) + + # Now we have the correct connectivity, we can regenerate the exclusions. + gro_sys = _SireIO.GroTop(_System(mol)._sire_object).toSystem() + mol.setProperty("intrascale", gro_sys[0].property("intrascale")) + # Return the updated molecule. return Molecule(mol.commit()) diff --git a/tests/Align/test_squash.py b/tests/Align/test_squash.py new file mode 100644 index 000000000..ac829a2fc --- /dev/null +++ b/tests/Align/test_squash.py @@ -0,0 +1,204 @@ +import os +import numpy as np +import pickle +import pytest + +import sire + +from sire.maths import Vector + +import BioSimSpace as BSS + +# Make sure AMBER is installed. +if BSS._amber_home is not None: + exe = "%s/bin/sander" % BSS._amber_home + if os.path.isfile(exe): + has_amber = True + else: + has_amber = False +else: + has_amber = False + + +@pytest.fixture(scope="session") +def perturbed_system(): + # N_atoms are: 12, 15, 18, 21, 24, 27 and 30. + mol_smiles = [ + "c1ccccc1", + "c1ccccc1C", + "c1ccccc1CC", + "c1ccccc1CCC", + "c1ccccc1CCCC", + "c1ccccc1CCCCC", + "c1ccccc1CCCCCC", + ] + mols = [BSS.Parameters.gaff(smi).getMolecule() for smi in mol_smiles] + pert_mols = [ + mols[0], + BSS.Align.merge(mols[1], mols[2]), + mols[3], + mols[4], + BSS.Align.merge(mols[5], mols[6]), + ] + system = BSS._SireWrappers.System(pert_mols) + return system + + +@pytest.fixture(scope="session") +def dual_topology_system(): + mol_smiles = ["c1ccccc1", "c1ccccc1C"] + mols = [BSS.Parameters.gaff(smi).getMolecule() for smi in mol_smiles] + pertmol = BSS.Align.merge(mols[0], mols[1], mapping={0: 0}) + c = pertmol._sire_object.cursor() + # Translate all atoms so that we have different coordinates between both endstates + for atom in c.atoms(): + atom["coordinates1"] = atom["coordinates0"] + Vector(1, 1, 1) + pertmol = BSS._SireWrappers.Molecule(c.commit()) + system = pertmol.toSystem() + return system + + +@pytest.fixture +def perturbed_tripeptide(): + return pickle.load(open(f"tests/input/merged_tripeptide.pickle", "rb")) + + +@pytest.mark.skipif(has_amber is False, reason="Requires AMBER to be installed.") +@pytest.mark.parametrize( + "explicit,expected_n_atoms", + [ + (False, [12, 21, 24, 15, 18, 27, 30]), + (True, [12, 21, 24, 18, 18, 30, 30]), + ], +) +def test_squash(perturbed_system, explicit, expected_n_atoms): + squashed_system, mapping = BSS.Align._squash._squash( + perturbed_system, explicit_dummies=explicit + ) + assert len(squashed_system) == 7 + n_atoms = [mol.nAtoms() for mol in squashed_system] + assert squashed_system[-2].getResidues()[0].name() == "LIG" + assert squashed_system[-1].getResidues()[0].name() == "LIG" + # First we must have the unperturbed molecules, and then the perturbed ones. + assert n_atoms == expected_n_atoms + python_mapping = {k.value(): v.value() for k, v in mapping.items()} + assert python_mapping == {0: 0, 2: 1, 3: 2} + + +@pytest.mark.parametrize("explicit", [False, True]) +def test_squash_multires(perturbed_tripeptide, explicit): + squashed_system, mapping = BSS.Align._squash._squash( + perturbed_tripeptide, explicit_dummies=explicit + ) + assert len(squashed_system) == 1 + assert len(squashed_system[0].getResidues()) == 4 + + +@pytest.mark.skipif(has_amber is False, reason="Requires AMBER to be installed.") +@pytest.mark.parametrize("is_lambda1", [False, True]) +def test_squashed_molecule_mapping(perturbed_system, is_lambda1): + res = BSS.Align._squash._squashed_molecule_mapping( + perturbed_system, is_lambda1=is_lambda1 + ) + if not is_lambda1: + expected = {0: 0, 2: 1, 3: 2, 1: 3, 4: 5} + else: + expected = {0: 0, 2: 1, 3: 2, 1: 4, 4: 6} + assert res == expected + + +@pytest.mark.parametrize("is_lambda1", [False, True]) +def test_squashed_atom_mapping_implicit(perturbed_tripeptide, is_lambda1): + res = BSS.Align._squash._squashed_atom_mapping( + perturbed_tripeptide, is_lambda1=is_lambda1, explicit_dummies=False + ) + if not is_lambda1: + merged_indices = list(range(16)) + list(range(16, 30)) + list(range(43, 51)) + squashed_indices = list(range(16)) + list(range(16, 30)) + list(range(30, 38)) + else: + merged_indices = ( + list(range(16)) + + list(range(16, 21)) + + list(range(23, 26)) + + list(range(30, 43)) + + list(range(43, 51)) + ) + squashed_indices = list(range(16)) + list(range(38, 59)) + list(range(30, 38)) + expected = dict(zip(merged_indices, squashed_indices)) + assert res == expected + + +@pytest.mark.parametrize("is_lambda1", [False, True]) +def test_squashed_atom_mapping_explicit(perturbed_tripeptide, is_lambda1): + res = BSS.Align._squash._squashed_atom_mapping( + perturbed_tripeptide, is_lambda1=is_lambda1, explicit_dummies=True + ) + merged_indices = list(range(51)) + if not is_lambda1: + squashed_indices = list(range(16)) + list(range(16, 43)) + list(range(43, 51)) + else: + squashed_indices = list(range(16)) + list(range(51, 78)) + list(range(43, 51)) + expected = dict(zip(merged_indices, squashed_indices)) + assert res == expected + + +@pytest.mark.skipif(has_amber is False, reason="Requires AMBER to be installed.") +@pytest.mark.parametrize("explicit", [False, True]) +def test_unsquash(dual_topology_system, explicit): + squashed_system, mapping = BSS.Align._squash._squash( + dual_topology_system, explicit_dummies=explicit + ) + new_perturbed_system = BSS.Align._squash._unsquash( + dual_topology_system, squashed_system, mapping, explicit_dummies=explicit + ) + assert [ + mol0.nAtoms() == mol1.nAtoms() + for mol0, mol1 in zip(dual_topology_system, new_perturbed_system) + ] + assert [ + mol0.isPerturbable() == mol1.isPerturbable() + for mol0, mol1 in zip(dual_topology_system, new_perturbed_system) + ] + if explicit: + # Check that we have loaded the correct coordinates + coords0_before = sire.io.get_coords_array( + dual_topology_system[0]._sire_object, map={"coordinates": "coordinates0"} + ) + coords1_before = sire.io.get_coords_array( + dual_topology_system[0]._sire_object, map={"coordinates": "coordinates1"} + ) + coords0_after = sire.io.get_coords_array( + new_perturbed_system[0]._sire_object, map={"coordinates": "coordinates0"} + ) + coords1_after = sire.io.get_coords_array( + new_perturbed_system[0]._sire_object, map={"coordinates": "coordinates1"} + ) + + # The coordinates at the first endstate should be completely preserved + # Because in this case they are either common core, or separate dummies at lambda = 0 + assert np.allclose(coords0_before, coords0_after) + + # The coordinates at the first endstate should be partially preserved + # The common core must have the same coordinates as lambda = 0 + # Here this is just a single atom in the beginning + # The extra atoms which are dummies at lambda = 0 have separate coordinates here + assert np.allclose(coords0_before[:1, :], coords1_after[:1, :]) + assert np.allclose(coords1_before[1:, :], coords1_after[1:, :]) + + +@pytest.mark.parametrize("explicit", [False, True]) +def test_unsquash_multires(perturbed_tripeptide, explicit): + squashed_system, mapping = BSS.Align._squash._squash( + perturbed_tripeptide, explicit_dummies=explicit + ) + new_perturbed_system = BSS.Align._squash._unsquash( + perturbed_tripeptide, squashed_system, mapping, explicit_dummies=explicit + ) + assert [ + mol0.nAtoms() == mol1.nAtoms() + for mol0, mol1 in zip(perturbed_tripeptide, new_perturbed_system) + ] + assert [ + mol0.isPerturbable() == mol1.isPerturbable() + for mol0, mol1 in zip(perturbed_tripeptide, new_perturbed_system) + ] diff --git a/tests/Process/test_amber.py b/tests/Process/test_amber.py index 89355c80b..600b4ff81 100644 --- a/tests/Process/test_amber.py +++ b/tests/Process/test_amber.py @@ -1,5 +1,7 @@ from collections import OrderedDict + import pytest +import shutil import BioSimSpace as BSS @@ -31,6 +33,17 @@ def large_protein_system(): ) +@pytest.fixture(scope="module") +def perturbable_system(): + """Re-use the same perturbable system for each test.""" + return BSS.IO.readPerturbableSystem( + f"{url}/perturbable_system0.prm7", + f"{url}/perturbable_system0.rst7", + f"{url}/perturbable_system1.prm7", + f"{url}/perturbable_system1.rst7", + ) + + @pytest.mark.skipif(has_amber is False, reason="Requires AMBER to be installed.") @pytest.mark.parametrize("restraint", restraints) def test_minimise(system, restraint): @@ -286,3 +299,64 @@ def run_process(system, protocol, check_data=False): for k, v in data.items(): assert len(v) == nrec + + +@pytest.mark.skipif( + has_amber is False, reason="Requires AMBER and pyarrow to be installed." +) +@pytest.mark.parametrize( + "protocol", + [ + BSS.Protocol.FreeEnergy(temperature=298 * BSS.Units.Temperature.kelvin), + BSS.Protocol.FreeEnergyMinimisation(), + ], +) +def test_parse_fep_output(perturbable_system, protocol): + """Make sure that we can correctly parse AMBER FEP output.""" + + # Copy the system. + system_copy = perturbable_system.copy() + + # Create a process using any system and the protocol. + process = BSS.Process.Amber(system_copy, protocol) + + # Assign the path to the output file. + if isinstance(protocol, BSS.Protocol.FreeEnergy): + out_file = "tests/output/amber_fep.out" + else: + out_file = "tests/output/amber_fep_min.out" + + # Copy the existing output file into the working directory. + shutil.copyfile(out_file, process.workDir() + "/amber.out") + + # Update the stdout record dictionaries. + process.stdout(0) + + # Get back the records for each region and soft-core part. + records_ti0 = process.getRecords(region=0) + records_sc0 = process.getRecords(region=0, soft_core=True) + records_ti1 = process.getRecords(region=1) + records_sc1 = process.getRecords(region=1, soft_core=True) + + # Make sure NSTEP is present. + assert "NSTEP" in records_ti0 + + # Get the number of records. + num_records = len(records_ti0["NSTEP"]) + + # Now make sure that the records for the two TI regions contain the + # same number of values. + for v0, v1 in zip(records_ti0.values(), records_ti1.values()): + assert len(v0) == len(v1) == num_records + + # Now check that are records for the soft-core parts contain the correct + # number of values. + for v in records_sc0.values(): + assert len(v) == num_records + for k, v in records_sc1.items(): + assert len(v) == num_records + if isinstance(protocol, BSS.Protocol.FreeEnergy): + assert len(records_sc0) == len(records_sc1) + else: + assert len(records_sc0) == 0 + assert len(records_sc1) != 0 diff --git a/tests/input/merged_tripeptide.pickle b/tests/input/merged_tripeptide.pickle new file mode 100644 index 0000000000000000000000000000000000000000..20abb1d6437cc5f51cc76b84cfdc3ed38303a5f0 GIT binary patch literal 65668 zcmb^ZH_!aqvo!|Z7Z@l~AYd|O$}}MG(z~#`>AiQ!1#Q!NvPl91jPjf5&et7iKj)8u zgjWi6lJoq!X|}1VUA5M#{{Q?x{=fhBZ~x`r{{H)q|LNUKZZo)ZnwS6e-`!>{!!@1f za^3#xzi;=pm&1Sl$A9Pk>yQ8SKmT9<%YXjA|MP$TkN^JPH*n#T> z|Js+|G{67rzpe$3`aj?7{B!gF^PhX?Chz+4-%s$OW*Yxf{V)IVKl~p*FVa2zUB;ODLw&xo`fc8J z(5IhC5CrGt@LY=!4YHY%EGoAAW?j#0ij1Ml13xX?J7K)&<`UMS?HpKKL!XDCP4Le| z$11Zo^j}gr-2)rTL+=D#{#IkSQyHRGEW!dvAkphkS%Wo4=yQjEEY zl-$frjdB8>PtNTss$Ceng@_tHh#7{6b_>oSp+5!`miEp%3H6l7u+rbvA8$)Y!!TdZ zoz)`lv-TKUWmtPECWEHhDWsa*;OKd3{t1`FhH~7JC^&uS1;1+togkaqDScXJ@FK0+ zm;S**P})4c!%`9$uoU5K9U(3gdKbK);U_E4VENb_%Egir4=o}TnL+gZCsvq+bq^QC zxzikO>jvn1A zj-rd~mut~U`s!2sT-~a_Ch@K2DoJ-^h{xt#iZuD_skQP~yn?b>`oh-==O_fz!_L#m zMvwFZ4WvNrRCET+vz1~~8=Nwyce&5GoH_7y&>Pdz*bYBhC{+*A)E6etM^`y0TBy8* zW{ZAU3VpEfx_2nj?r$v0&gXfZ^NvLf1cz072xD~|>vG^3z2#g|2nSyfa>&;Xf`EYXM~=mkw9CtLn}q#JI!G1%pF z#99x$OkPBRo0ZTZ$?}c;m`G9~JHM48|@#RwVLvpygK-tZ@{Id%9M|8F5u~ z8I6GmYly@mQT6hQZ+zNETzTCDJ}Xya?|VT)c<+{ZXsPXc>%=NQ67Dt<_BSMXYIM^> z3!Hp(rHHAXamdU-tV}~h-RDZ~E~W!zz=PBUPWYoPnZTAxKW3z(U`kl6eAl8->V*OC zF`Je3o0<)~@Ug706Ak_GT3C`{c=VRKa=W_KgrD@NOKrrwK}3fzbKbAVk5iAAwU8>Z z<=`d6tdrgnCuF^ocH*}F@1fuH{k{8RE=KHQ^mVHiCfkne@2V}un>GbJk4$FV6(ICm zDyv%k{IgJpEYRs6jh1xAmRidCSiBKj8P7wMs?b~Y%!n#3DB@yq z&M$i&dYXCtq?rMK_pBt4>m4n@CX(_^F@%eH$UwgX^%BwPUT)geV+`u*j7y?H}!Z zb_b7)LlATJJyNYJB82OUZk&3x;U#lDnLMfvsifz?C3QYF*&kig%8={Bk$k++Po8g| z#PR*Z`qVD(u_sm*1!K8?_bBD+=02;Oy2FzVZ*u_WNZc*Nl28ox5xI- zlThA8e-@*L!k-#v6QGKk>!-ts9`>yf}RVBxNZEYGeD zC-vn?vn}1sOM>W8_VFTtgvOcfv2AWhJ#P$aAoRRLw3I879MMEH95V8E0|`hfjZ4)*4<%6i3&EHx@5}u|AXR?OX{7oURU{baRjLP>s>3LZq`Dj_c}&26 zE@B3U<->k4>n15(t`J_6@SYzJv;BnR21U9Oznsr8Wuu#sSM;r*swEaV$hV3h7zF;2 zQJs_pIK`NwYz9lL-!(LJ(sd7|azAnY^WolB0(r0&ueLArE-D$ENcl~-I~i4f%TWQ} zBDd(CI3&_fu@X7xw&JsD=3YqZMhNc*qy+{0V*{M2le~3PwKq#vJds{*wr|j*hX;JG z%D%ziZSWw{M0pfV9&mP8!N-1_XH)oH6az6@e5>sXXCL8$dbykxf7;v zgTTlKYLEoBqI-$8bSo?M1es}ntn+iSSW$Kw?<&BZ%$4~d3zI5?+UIlU{ z@_G<9ZNn~SC3m275(i`Y3{YEA9(t4cOv7upIb$*lGcCSRgDcds zYRadGA4MoAo-5u};t0jN`1Z=SMVTr#V*D5a`+Ozv7E?6v_p9$y&D#dY%D<|%YkC*q z<*S;iy&I12!iwX-+;1JVdH&IK#K%M5L%j2T3;ULY5)>S~Gdi9< zJ()?YxprYZ?NNCpeG{nJYH<6z$Q+}1=ZM`J(ISWr?xoo@9^V(97~8ZTuBL6p=eBBM z_JM+s9=4q!e5T9D{uYPENE)ug?>9#fxMv_y4;1jGevXBYc2-WeXaBOw=cop zxTwS^OlX64N@RX9#E7%LBH6<|Hm(QUTW+`2$6c8Ga>a&W&4PU#B>g3A?2fAZ1kcv_ zc%&DTb)v1o8=9unpQDCk)Ic8Ge9>ozAwl@NUz(xw-!~qzLvs)Uw|`jVlTxbZaEM=u z$&JB1SSuV)Uv%dO_E>(tbv2sg(3CXTFO6LV7NL!qd9w1BbT8&s7ik7cw7x@1D1w1# zc=)1+07HBp<;eV_quD=f)pdGzAFRZhn6nrQjIo0F7as20H11m&v?Ky z(0{!XcaO|zW-b35E3AXZWRl9!3-LHy=;EWTQsLc<;gS5`d{@F5T&a79Zq2A@_iKCBiP2n^XTPzwLE4<~YB>oF~7u{}?m|)%}W~038N+TP48%cSQJ{7VmQ8jnUua_sxe$#yzFfow?CUM-@wn4 zD=J9CZ@xKh2cd8fC#fS5{(5EWN~daOa#g*uFR!|Fzg&G|NHse7Rg*P*y~qw|1Nq=7 zXME4e(1tD5iymf9}~|EPx8A&pg%NchP4)Q862c z9r>RR;eUH)$fo*K@BDAyem{{Ku)A5PSMm|i^!@a_T{6&k4-|~MFM4H`+9d57EY%12 z3PgceDZr{IAZpZwPrrqUME}+#GXKVPzHq17!MJP?zU1E+1knmV0MrPs`wL;KNEzxp zk2b1KPY`WjT*cqGj_Pk*iBR>Z%I`y}(x@#`8RmlZLYXOBN+L)9)?a&N^O+F8gAE&G zUPSiWMVPl_(|K$!yvlE9FDfy`rv(4PU~S=C=8pPrZJpY9Yg%Ue0%kFjkH^g4dzHV< z0x{)9UrE$p+zF*cGWBPV`6a$=6Nq`k)1P$GL+Y8sn(m=sdye+bbtv@qVO!QvSvO_j zCz}zB-RIxgxxFB;@XJvn(fm?9TKk`kVKT?8a)I`@k0BH%Tm(+W@ zjAp?bBJIy74A!(*0-wvj zV4Zmrd857@x!O$Qbu^$qkO!DKuzBRQd|iC`m!Eil#^6feV%+(AK>Wph&%eRoB-4L` z<0hN9TDVQLWrVQGxAHb`5DGVNHc4UbMRDXf=Br(hv0Tv@wR!_l{``khisEfqR+_GaZ0QNDt{2dUx@BPgu1bGQKr~l%)jEQa(Op+N$ z?~*24U<1crE;EK$Q&I&U>W~Jn?foSiCzsy`V_)9!I%sM%lW9FP;1~Gg)P+T-DfBsy zfq7v+jQabIe&HUz$-tL~SO#GOe8){yOL~Q*gMA5{IWy&`t0r}!g(X=^HJfR(`$P^% z&R#tVU%lqY``m0}iDKN?W`GktYPf=*hPZfBwedke;AssiesypH9^N;|AOQ&} zf|G2O<2x~ggf5I^PbQn_E|7#C%>(O9tXRP?mvy?fZyx(`IqTijd)cCPz{dw;UQ4J& z{3i_7pGo!&0x&0G=X}y4Su4QcM?_TVHoa#yJO7b)X6nmrbzk)qqyM`0#gBFZJ98Yu z0IGUw%ARc>Hjq`7kxje#+Meco)6(~>GNQ59acdd7LK&xkXk~;5#$%s7-Wgz4s)P@i z)^&?7C<@ZCnvBSeNjjCPlkd!d9yvYSuYtIKpE+_?^K94ed<)s(|NfcCYLw|TtZbhy z)OPU+&?wBs?nS8txpYOssr*8*8W(Rg%BvN+3%bMt$x_ENT|ITh-I(pFn0|+)cQ^}r z$tGYh9m?bPr&idWXJy1KXT@-2xZ@2!0^qH0v1+A{Ya^6Ow5CpZeNG?vxdY}`B$exP zkW3@3DN9F}fcuCBS~N&LCq81GGWnD?n9p;+sq=>PNO8e=k3qVmIO3v695t_ z-^Wy*#IEp{K@EZO_LlK$SombZ*IRtI$Swb7V1sjjERkx%$xr4~g4=?xV(7pjtNi71 zUs^+ukJ(Th&sY;rvLb=J<|6ohKkgjpkp7JY7fyn~H@>&h4(;>m{mBu#FW*iRYnP$j zYTI1(9c|qNIA696DVbM}CfSq3Qh%=M@GF$`^tR~um^O5Y0OD=S!`q`h^utole5;vY z|J-iqzz>9bQ**DWYRx!!$kxfubP%BMuC#EM%gRAmues;hjDg=B62!9_J;e>O!`trf9^rM+nIm z@cGQS!zr;eGoHx_2YDQxoZ^ES#FA{*Adu`3pu(ELI0x2PNLX5^~Abzn(jOLHq)-t}1N;Ae;*I#=dzX{rUGBcu*U& z9pqTpM}l}A+{;X`I3O*=d$=n;3Z=H&Vv(}+w;nGrLz>c~*_EiV*2pQs)zr8R_Qk0Z z)V+HS{s7e63O_Q#hmeya%OB<16@xc6kqV1Noq@H8@UAl_7sKq0&IPmHy|3V|`r zZ?vZs@n>7#FL);l{s8fL7IRq}0wk~!zZ#rKA5*pP71Ta29z4ft6biLql~TKH;hyP? zzvUf+_($6KT|+^HB-SoQ$ggzTz0}k%xb&M?86-d* z^4j-sPMr=B?3=aWEB8nx-X*L;z|M%{21W3n7#*sN2${TOOB*LhU=w zJMRMsmMr&N#s`_c06%I_`Y8mb(q3GKC?L5d1_&*P33oDCsL}|z)jg4?vafx7Le>ka zm?{qn$E|NVc4c8r_b!Cq7SCdX&mNCnjEoVsCg3qDye;oCY*D!0Av}&eP{h(;fIWcB zU;#N1!D?^o*3JbJ;T@CkY_PPeJU@3aa28}8I3`ul*b&0zVqG}GAo1? zF~TZX@bWL)vOm5p7vo_?19=rYTb&B5$tkmPLE9uBHWjYrm*Z19S=wm`Z<8VMn;^gD zyS-~hf%5FzzS5!X;Mi!H8aA2w8+RqabP;b{$rx99fL z$pt~GWQcUk&wWFN4Zy*a2?2M_uQT;Qg0}z&6X66<;e7|=QQy<)^}o0~AmSVOl{hxo zvN1|R0u91>|Bk|t!siL0^JMV~6hvQ>@0pWOQ%B~5T6hy$AN=;|>@*#mf;oJgX4O9^ zOtAA?R(Z$Faq1#nvqiPU%Ti5xIvFfI64Ta)w+Q53G{E6PMw; z7l72zSRA3l>jX)+;<^NN20mdI~}Q|-w)RG9jHVEw}D0tI}P!t4F~+a=kmP};i=xbjK3 zVvDNs6cwK`z9Pm@FHS@*LhJ0Ay-@6mHqkt`QuaPgMmt3|yHvI$;OTS75?k*IA4A>_ zj%jAApq&04P==oT+COl0eCDsn$78EY=WD>3&AJNe)xa4#N-O$6-_u|}Inb$$LJr)S zSj!&+7EV7Q;A?4N7-G0=~R7Sd&Zv zSXf59OEj8um>j+rBOJgc?(@y6azZX)?aq-xLmC^C9BBByN z1+{x;d3ps5 zQ##UP{g$|Dp`@W;l)~vv+P<-48RYm|?9uuf*Y;vfUQD>x-dE_%!VOp>*wnQX{XRc% zJg?2~=gBw=7cy66G5v#3U4$%&)Rpi;yh(x#{P<`Lt#2ReLCOhJ2s93P?v*xa}?^((?k^mn!1Rltf*1t1WdHi7-_^!u!@1HFXj zp~eP<^zp=`Pefehn0iG&T;qtB?~Xy(@cF(_(P8la|2t`|<{5C7#eztF2g>NbfEC%W zdKC_brxcGf#EpnpPprtEaYv}o+7|5B8wM~cNDhA!WU?&2iW~>03{;I=0PDw}PvKSd z+(is%KpWp*zle#tL5Wy^e}O~RHu8W!dw_MiFyMoja9i%WrM!W=s-59IAz>AI7nt`g zy{QK>8DnW0H23K3z>+D8UKoyt(un#P(w7=qfBQloVRdM;C15|@*#ZdxJsqa>u@W>@ zspyFJ$e~46@BfK8Qh5@6$~becF9;0M6bRr1@ENh!Q?oS5!um=D7*#Fmjl=ruUDXT) z;!w+3FRfU@dEId$d^2#Wunh5SU=PCE081;zN>^EegRv!{#X`e?9^WgyrWQabXs?J> z3Xdz)i&9gUSg7{VXZ@@Ukb4~gU06-}dPP+Xh#X@ie=S0y-`iru1w5Oj;r#5qP}@V? zk2CAO;LO2m^q+g9+o1{G+;5(nqiHQ`Z{iK*we|r96lZuqWchtR50#zE64pVvY`81O>+CzOnIUSj=01snMP31(VPFFu##Zr*k$GcjiD4n#u(vBB^Re6lDj|(clXThO9eVB_hI6g-`*_%G5Hyi^oAagB#p`TCXCoLkc zOA6#4Q-lDKXSKb;pX04MCyE-3d|WbKXkn2yQsBzeZO;@$V5O=wZzwVk%tMdEvLOEL z)-93~RX|4B)szaSaVjin&*CPB-Io9^`wa)B^~80Iu=QB)l!Jm5IAf_t)D2j2A29sZ zKK1^+_14zfDME_-T1*wK;@a=}!o))X-##OAqe zQ}(tXGprlB=#d`vg^Hy_O|35*`~`9m!{0LPxynI7lBlgZ6e|>lTS3M}KAGX}k4OWT zpT8Xr!~*6(8pl~FJ{L>51t`-(8e_|VZ~ z;+XYa2?-R@(naHy&O6mqWETZ7&SqW6Tlo;*Ut*~xLlOkL!gXh3*=Q-3DtiHD~7!S0Vu{**%9+M7K%Pq z^=)9QkK#%aAPKMS95{kbId;hJxyvMZ$^>BVf7(1Fh}#(;D4^XKeS>j_0_OE3FU~1_ zHWHfa`B6bFRoryE9`XvQkMteLWPGbIG1vPuDM^b#EZvxjm*Mf7_3&H$p2R(*ed_j%Ao$wJaFJB7>$^FJ{gX9AA?V!orK z0BzJz&9Um%cjal{%?LUq#8F_`m<`NI%TsEvrQRg`F6Zml4QyP`3bk*H+Vb@{&X?um zbfgdonTSZ*dXSN(Md)AGT8S@YMc=ddGE}(?Qs`=XmaMLsUxW%&7A>RpeL)ffkk9;S zKulET-f^B1D&3Wv1!M}a#mfPc?YEUDg1NrP5B&1jVC>#@?f!CDs*!cC9swL#fUlc^ zYQIcM?Y|1;8s8v9c!U!3-#+Ns`zK*O`tDQtNr9dlr~`k(Il8Aq0DbZbne|#qlylKQ zkU5z@sXhR5;YnvV?aw2z>cmz_%9I)?;!wB20{^D>H=L!-xmN}avWCyOY&U7j^&XVS zwD$ojtl|!Fz>=!lGge@IDZ^Glw7@UrEHmZ>+d7qGG(w$1`uXlIVd1HcmW*oNGf>>xMX!C zgt69_-2*=DJkTiin?cuk8X{$?b_Gq5=m95+v`82AR@)%=_NFJjPrDy;xPS4PxY%c2 zQH%)%*>Yg9Sr&jK{#H;-nMLYod)aj?NV(oum2((Z0c4j?7{-c5a-Xmg7rk4>7?-<{ ztGx^q6vUMg`^}7nqfoYZa^LWI&q8etq`kuUDF9KfbiZgsP?EMd9%SUclZ=^9;~R*b2{3F%Q%Ti z1VsO21DNQnMfpA~U^{?bS^RE~#HU}NLh>*JYXuW%>a#lhy2y-_sF%f|jnR1mm3 z5`^4@EbRf@YVL3Bcrt)i!A^BGb&&TsrAJINK!6CEH8>9IA|+ZN1uCwSXryi(P}v8FC9*ugk48y*a4qvGRi|q-hP%;O%+=UPJL!kO8N3 zQP-hRg`)18KN{3>TW-2w%Y5(|W7Jic#iQBr-&D}mDwqurPXLUIMnB9{!os1AV!eJE zR{{bT{9o&Muplw1p%D^2@5U2pN`LO1M5_ZytqC5!MwZ zzba_S9S&aZG=l`Vrx2Pm?B#R)X3UXM3|65S-Z~&UFLapk+KLKRh^}Gc4|3fdT4uGR zd;&$!1q6V$CC30)pzH#&(FT=p4wmeyr5!+ZbQXndClTu<;7mAwxx;h5i{z zWRbtcg4a}5WA zsX7(69GWyOU`TDI;Y4U4TGEF*=o0`XfSwcR$;He%FPP_^6`)4q%GzkR4h%eV0?|vc7PS1mPr$f7$6c;8g&QiUZY50Sh*%r>SZ z+aj1Fo(Z6g*bsdxAt@{eeSX7m>5v*fYx%lyRBG$|36@g$VRSb2t$BRTT<+Q4`$Q7; zOK#lI86ioR70|CtF|+CbG}0Leg+Sp6@YWJ+-U%B657nnBP>fdpsM0s6=T~A3T3SG9 z409h9A~I`Q@H#IbokD~kX-`+$c%VyQT*KN!44KJ*D&JBM-F9&43rN}aX9|}qB^P16 zh)dw}<2{vcgc5cnpt-CNq+>TiB1W%9dPmy?j2V-8^wVIMMe%?msZC^bGN^_AqCr>^ z3A#EL^STgeu&CC0M_Rf7YF48BZA{qO>K$kF0Mg;f3p!&!Iv)d#8EX%Uc`#{8_M26E zWC8?>8Wta*Q3sQ@D`z`~?FkArKu`^US#v6-73psHUf5c3y-;+x)}m6!2~;fiAWCpb z$I^DF0PD}{1K~IG%uME3r@}ckV$gUtBw!%-s29v7jfM%w!%k@c12zbTL`wM~n;xQB zpa9w3ga$}fy92YwGG%J%_H>Ku0iw4$0j6N@r{799jtk@B34lI9pvOJMnK-paiCw@c z6@a!wtBbz4Q?aD25Fr$jpkyZ?ImoQs1bjybX(Pq~shtbEW0QpYJhzvX7uOq3UI$M` z2)Z(}{AFD05v96SGV#UMpzEYWfG&mbD(YZ>)`nGBsd!}=0~EXA+4wk%rWDx4m`y61PPL&yEMrlVe8sg}_OPzyM z25aD*YJB+*K?7w(dX8JgiqA-XhJYeZ;BmVDUPCaVF)OE51iB`tSS%xlDSqux z1L^G#*h5m2R2cfK2$GiG@bZV0o7^tR3he5~Oa;ET-l)i`((E_}LxcVoIezTL>0;%l zr$00SSQb8&NzVLhz6%C{!9)lI8X!qe>`1`e0U&7i!eVzI*LSGs0poXpkfabDU%Qnm z;FIbh3Jj3X&M^8`fNGMgFD;YxlJV)y@kcdfD^W;esCwU3`#E_GV>FFJ_e?eZGwbc(4D7U6d|@ z5zwWh!AdoFD78vVe~a~1*Ct-pM1pWwV+quE%_zX^(H>~G0~#Xs0R+5X@eD)HEbT}g zVhew5jx_{L-vL4jt*CWmjP!w;dqUwkyb)LFO*|KQjDQWhF=#EAuXe{l2g&tt67j|L zo2`Migz5VoK1THQ-mKh&k0@hf#0Oo^mtOJuTW@hv(eQzE8T7SMJBj`(m5M^2;xf^#PhrNLP~OW!4dZ&+(-ek0mZUEYPf}Q?&CPCdX9Y66MKj(}Q}< z+$t^92RmOq4S-DuU@tiP&-j*p{fAzYyi4-m~SJ~ zwO}+8HhrnOjC2oSkOvZTMg?LH=3E5eXL=)nbzkZV)iwOQ3#N#M2SwgA5Jv;52Kd+q z<$KSa3`|L_7<&J$n^faYu$&_T+>-avk_DuL@6Dn!_MLT> zGBeifUUR^gGVnbXB8fuCkH7-ZE%+ziZIUG{GV=nO$f@5vhZ2#4YWCmUC=SFv?cG>Z zm}|f`@aM-lePn;HZ+DfmZD(l-;wFs$J%{#d2rRkRLf$9>9KQB=Z~z114%oBEe!|w0 ziRxTrWdiK(=;EZ=JzY$fh-c(Q8!$#C=e$AWlS2S zD+>hPIX=>AETf+Le~wfP-b;Lg3EM&;cT-r6-=>Nm^!n&3Xif$o89-P4f{b{83ck%< zf#Li;1(y{w^FOxdH=ORNK(aRc9=WUzamjfTYbU-S9bUpgJQ2j*b3i_4PO_*g`_d|& z_=|@$D&yD&b$$P4@)huMeI;t$J5zOhk+FX`(Z42fYdp zlQa@tOF`Sou}taAXW^k~0PC0b0bq`V>e1@D8DQZal`oGYw{vY(L)pm<(2lQ4F zt=FJw9f-$e@B;-MV5P`d0y=8}?@h0jAX{J#rgPnr>$T+{O}AkAv+Yu1mqythqYTmh^9H1qwL&CSNO{kA0BD z|J_5x7ShUhnY<+B1vIgLT2lv}c*X(qgF!nZ(6IhV{b-<5{O2)`1+p0y?+`EP_-i9$;@7pg4P1e@E^ITO;?w z)aBwviRJi3??3@hbsqX6c@J4;;s&Zh&6jSGbjm==8{ldzCbI9-jlr0(e3)RMq3!qo zR--yE2+#{oFbLXzay&~0ngGf1(cUhLH_h8Lkjp3(()B<`3#JM5u~@5tKCchpn!`oO z_3?Sx`q5Jw9%4&`b+@ z<$zpDb_B>j!4Hp&Nv?cAZkPgrpqXx)qjXa6-|AV-|C{x!h5CIj15lz$pWoforSm>L>6n=5TUphbpC-G4JW|wwBYuNh`KCMp1?;yaSZ_T z5NK7A$aO$Tmbvm-oB{A(I#)PW`_r4F4aEZuVZ*966rCzFz#5RN&h174m2ToUkPCjQ z;BZR9A!QoUG*Ie#mLJ^)<4epw5$qG)K!^>fA!LAM&>@_s@)z2@E!DK*Mf%kt^WKTn zs3Cp;c?!$lb33P)#fw;(iQ^BBr4*AuiJ=5Pa$;LCfUb={eY?H;mH*rdrO04!_^*l3 zYX#b#fZ}uq@I>;zG4?0hs%l-dF8V+uhy{`c1QIKu7kzYPlYBnO>2 z-<{ZhKCmzv&q5tf?f=oS>imawO67j5h!$?9-61_S1c#~ktvt!{YKz?*Kdz) z59FV}cd-`dPB>`9@oz8_m*VB;SMq4@`qTf>oO2MrLnC@|DqZ5WfBNJT)j$^qvBZ&6 z|I@O1`?VxX(}8UKAm>N(A)n~sN>y`kn!W$@=QZYoy(D@Gw7Hm?#$QNdgdU~Epg+6%j~SKb@}L+qmrU7jebeCo{%^|Hk$2hV+#ZvX^19!V z!`eGAO&l>Tg$n!opg48N^5B>-$~U@vj(mJbtCTrj-e*XIhCPI_{c&nJ&AiN|v`aFj zG28EDy}hM=U60W^Q>z@=Z)RG*vT#oJ>u<;OAx$uR7V`UG6ksY1M?69?SUkVKAJ4Do zZ9&J~o3%iQUOilWwKKLiO@kB^GECddlEMLVQnn3!g7EfqD{l9;&CE|Qs8!2D2 zPXaYXHShH6{1zD}WgqqAoBGjw<1QVDU-EDZ=mVie(EJBWiqDvAZ0O?%i%zA5*Vi2i z09X+Tuq=NBvpJRW?fN@19i9}sE~va(n5+0v6yw<0;$7`TZ8Ll+?Z&3!$X7P9E6wLU zNs>Emka!4>ml(eA#dcCydb$o;t=Pr7qLY69k%53Fd^nLbuIpYPMVM+#VM|4L`3lyr zM({!S68(l=>r?hB84DT$l)yuUGC6bN5VqR>t>|1#g1&7P+w?2Jm*Q3W@nQ0r+rErd zR4Mh!5+Yr?ao2T4(YcZ6G45h-JsHLVeDkU6teq=tA8KDu=2wy=BkEpc57I&-q^_s5 z%tSnxW%fBb4h<1}8uGiwrE1Q$cTD8Ww@=sl?OpC^HhCY^w~2ww@@YCzm!@~3>U_1G z7FUGHYW02dAD_jWF1K@?5s?EGXfd+PzBZer(dVK8L$elb!O7 zr7t--Q;T__Ov~*VE+%%wP}r_O7S=ayZFeJS-Uv^M@DNFHJD{R2iq`m`emQg&m_KieDE#({G0hhuiMxo-n_PclS|x=#B1wpF3)c#$s%GSkiVY29h8 zELP1?2RYuBE+{m)Z@M`C%<$R4lPY_R7T2C%Xw|QfF17_~7?@s?`Fz^uvlfpAwD)^z z*-qcO=j4vZbevcpLeM3qB`PXdTXh4)WRy|Ni+5W=;GV%RkCq= zgKqVds!l9)j_!Vs#AS9hZg*XFPzb3SuM)TOLTI?>X6UB+eE-}|H}Zhi?_9pFM6~e# z-oL^7uY_j_4wOsZ=jnNjDTWL->2)6+7(VynraIPvNcVYqfTY-Cey2^<>x};%NfA25 zmWTFi-0YH*@3-Zc?)FF0`}ccjnrif?^JeQ`uYJ0Ezepo#{xqpXu%pRbctUTPX}QV< z-+!7!IIypi?=Bg0QHYLp;OpWNu6cOqai|MTWt^F(XY$8v_-s!7Ms$IqC^TE{pWc;* zqR53L^Sh1mYJ=K=V@L) zra1=|vQm+6@B8jYFz;#q37ur)Umg6=co-j;`#$DH7c0+H^KN@`B{=Aezni;%iIO(h zRZ9}Vep%6eDT+eP_c8RTSdo+7_9GH`B!`E=-6hg40B2ZwOq~HVj>=r1lP{4Cv}8Xw z-p=8UJIT;Ew-6VmdbS;#* z8~9VgzhC>AkMCXO(nA@M5NM~1_dji@{HG1Y-=FO3fJyK&9LOi_i29OU{_ZV4zdnc= zzUHFBz3D(kL7HVY4tj@22>ev3+qDMdsB7+qkuWMR{P}*;`5t!2<6*E~JX}Qd3!2t= z)5_rQ-#sj~e>OY2FH@A181!Q2(!CeG(m`StN}KT~NAetT^evA)=rI6-un!Xw}u z#E_zQWd9a{<;l=vb;WH|-B{pY&WIo=V4k-$J`@+{)Xu-+}(HCYzxFv9tgrmdjfK*m=#y(`q_>Tc}GN% z_xB`7ro`ALk^@10mRmaNxDaqCk7uf2VI=82SEQR!!*kw6CCq;VJ+sK~)P=2*U-Lgb zZ=V&LLD1q{tfQdag$VLJlNCkW|Jk-9s}Pb@UT&X6``!7__q~JN!PlP#A7)KVe5YwM zHLRwrv&hWTjppVLccHhx%{WW=$|FSsx2qJwSr!}ZYX0@xtpvS7f|{R-8+UG1EMpwL ze!h5t^u~Cx80Qu1XpyVoLj0ch_E_-7SY-ieQtY2z8iYz{%Blb9PqhWSG@S1GTD-Cj z`uK%GH6MECG?*bvJ~6~eW3WCs%IprXrvIxyrD`$OB1_j}MMA^uQyegaULZoYVxouM_((%hb_ z=;IPsX4gFxGKOCTWX62TAXrq`j!lw}w-eZJ;@ZwH7|SdmTo0ZfXiue^0PU&zPkXvB z{H5C}sV?IZb<7*YD<%0BB>u_yc?VBM?n{nMTDjt*&LOOD|2%!p_{6RH!j+QZMt85k z4?eNeBct;c50|5pBYrs()LPT4>`?w^-_6;YXXyq9A!F1bd~pKHtGpMJA+9Cn4J zNkdqGKgAa@+&~3WJi5rTGMpCOD3o`k2$0=T+walwZ zp^YfK2V`!`|6jV(2G?&2>wmga0;g6q-{rFL=5s1cjKkf;;W)Bxie!SkHHNF6Sm<)- z9igQH-*;?GI)z3O&Mt5ESK4`wE@xzOHqN7Q`?9V*VxAE;@^6h%+=&Da3{fG?y2RX$ z>?bBr`fO`D?<(TEwdbGfqk5c1EEK7bdJs`MFrtBixKwys1?gVX2rO{*N|Co>& z;TiFuF^u$-`ATbn)(yTHm5h zejmpCe&88_Fo#U5f(p&OPkqw8F}-zt=kTRJY^qXmFX4ROuZzr2_fxzzEN9T09=}Cw z7{kK-QjRIzevJK4ADMe3=AT?NM@WZ1gXHhdGQ0{)8A|i`!?GfN!6f=iuO=a=)3o&0 zOD*on`*`f+IeSWFltIfDNns;rGj{rR_m-4NK7X8nCZq(H!F1W{_dQ z`at?obZgB1?x!)P3GSSydi-pDcg_2GAIKSF*`V%h#5Je>=ErW#1%xJhi*CZJ-8y>w z`WU&(4E{0VPHLZ?xdwWWo?}N2u2Ob^*Y8R0ehg)NwLvYu(~~niE^O+E!=Ihs&O`B; za1ADa6vX7`$A0l#I*Z|}fgZ}c7K$%=?6+0CUc4=+1ouBMI%@3-h1Z9Cd2P3$D~O%8 zGqD)#>jxTDBUXoRmo}$wi;YUQcH3|wh?@gfRQ1cu-7LT3#>?oqyxyQ#m`=9+Zm0#m z3(quOT=4`$u6v4xY7>MD<`o>lY*FgJ(=FFE?tUf()xZ?3)syiHxRH6G3`{S1-|N){ z#w0E1eh`&@Y*-?_{?k2j2BK2v^KLGR1 zDl;E~mw&r$&ri4Ro%o!E9RU@Zbdm2Hd143sZZAL@su6D6xhns(gpB^sXcm(mkdXHB zk+1}|A{GX?ts10O_u`Xkv2aLhT@MJ}hXFR!c&$wDgxtDgLz}uS!h1-tgG_~%z z-^p;*Ls!Zrjbi3qe<-VYhDP(JZa?pli|d5iyejqEx%F=ESO2j(t+qZ>L0u?*N8MQL zbmWHfq3mgO$G1k*0BRfX6IUfpkfhM{+X03Dn5@-ug3-H?sn z(UYl?p^-$a6-dA|IRqd3+O%p*Zstb_hOqhlyddnmq&^i-`UAQ}=I6Mlzc}|RcYhhsl~~D#d83o^(VxT% z2e$GCyR`I;f=pg1UNXn64Qx{%gLf;~q=t34-fsSYiDGzIvYib;wNh^`QwD2%MTYjv zOGCp~=z?DvKblv;#7XzQDcs5`F0g1_UpedynYOwOZchIl_YOD4Y2|e{MrAQTPYQz4 zn^QvMUO`#Z1ASiuTEB~4(r%N9$-Lz3X7!P-?W}_QcE43|(MhN^aTNLw*TLYleQX*I zb8T2|GIXm8gVxv1<*;{^WR4`8JK*@_PX7_45zK_`soerAgiH}_f%)Lul8m3#>jH(G zjwPE>*W}~X9DfoEF{yT0t0l~X_jh3nu7@xq_7|y!X&}BXA~pN_*&kgd9Uvwxu@cw` zu%ms$`zXGYd<}7r*15$V2FZ0De*?{V*~$A}MG!-6Kd z@UGZ%I4IMn5uPl0?|xjfUwZ3v-X++3$iFy(2001Zya)i1SE2m=Q*P`If)?*aLtnxh zah;DMEURY*hK0{V0#2V>(sX6G<}EBraSHIWv(Q|Mt@ntHV(Q6mF@Fj^ZR6gS%<2=h z_wCXCBG;#~DY}hvgLHw15Rv(3LB2q@#=5afuw8mw?mPcBmQb7Ttcm=5s>V)#1Pop| z>XVW4IML{GllF)uWL~4prz9G(luNN95purxs zj3^yelk{bg@8AC7yh5xTz>qcl{c87Jb^5Moy*$&)k3B>8`4eCSE@yiVc38BeuTf~N ztbK@bQvw0{f|yY?h~#mCe#}p&`ItMqn<0HGp9wAfQ3z;uh}7@f^!-4J2{z9ywTn!0 zdTPTCUo#cPT`6XNQ4YSL5}fw>&au|>UksBUd*4Ei=*~W2aigGga%^$}q@0}lT+6Vc z<(Kn9eRRU{-me_%L+BYkZsiMTH#+aM=fi!~&Q|?tNh`zR9uQBV7Gpr(?MPUTvE#LY zF#I9UYvUVk`rMueJq4KTLY8D8+=|AeoZaZ24p+YH=2nR*OWQk{1eYJI$<08%S0Iat zOfxVKUh!=c>aQIWY3NV(yU@`fPY8^4>z6iq-c6Sdi2I|+VMZ}+XqA=Ci8sIp`ULuC z8M0pu>R&yVefk3ARZd4}F@^0mPO0AAmV;5vkdS)9t;4*)LPB~NSQ#FO6d=s;cJ-F= z+nKc#a>ZSp%9-`B_!DPHNOS)S19<}2!p>xjP;gm@xOuY^S3Y-0m>iO&t=!o;s0)y2 zAck}zhqQ^-EcQ`;pGCk-dbaL=HrpLQI#ER!YGElz$R?z zMbgj~_S@;`&}j^n9_srpkGkEI(Wf3&rh3*B;sX%ytHr*xUEWkc`fowM*-e;NK zM?aLVexGRPe`Sda5Yq9gzKBlov%%R3kHdFpV>EkXP;-Cr$oAF6U|337PU0{Y(=7P6 z&-NQD@*u0H7~Z?LvAdGX^!Si9)u-rxxM$%G{uE>F=F4w2z>!-e+RTyTkhh{Aj2bibNx|{4?dsxNz{C!SO@;p~Yz5qql9Wq2-3a$|!n9SV&A}|s`Z=MC zcLVKF4w(Z(S(s@{`)H04f4n3!R_08W>DqWHj^aSAH&LEA*l-Ng7OO1$mVK1Kj8V>Zzq^+*0rD8LK3;2 z-U{vwj>Kz0iMsR)3b3n1>m9)ueDZCx4j@_#MTA5?=5Py@E;Mz@?iwH6b%^#C?(N>P zZ_$xKZDg)X?sH8f3vyl~{j~YZ>oFD33O2+w0qypU<041FQ4ZwRTz`Ze; z5VFPO;ND_86v;Gz9hD@cACRI8KzrwhUu1rht*hppn65D@eM$B0^uyM^$I5#=6s;H> zfH;ZQA|hCt?hik4+QrETxIKucOnjVZje1zmx|APc_ZUBu_H~yt@!e1n*7n$)_(H|7 zdP(jmjCb*piS#lGMCKm!j|43niUt!j-ymq%#bg^ zh#>e$q~5&3G}pI4#N&mScvI1*W%pD&uGBIM7Wy=*KXU%-J`cwC!?^yqli}6Oa7U;E zx-aBEyed*RR(MpPwm2_caa!5J;SQ7UXejD4kN%IBr`j(!8Bl_czp@5pW^oOq- z6V_G}#^csEV9KAXdIq7e6me`jb8Vd=K>kB%o@}{WY9B-IISFdQ3l z(&OA*+hLr3!leV2lLA*#lb&wx=0m4g!fC)~MBv0;S6?yh7$GjNN6^40)+y%?aq)$q zt(M^1Sg3O_up7s=^F#^|rM;!`-nf73w}H+S0baG>K0hVxHJxq`y2T~Jv9iohby8;b z{H)tN)nqk}w_5}Oymf${*we)^2Vums?Xc@w5lx%j!TwymLNT%eYrW6U>xxw7_+ZEJ zuKcP_MD1{6brbV9nQ>^kU=RFG++U~IXR1fBqi;dkwQ}r5R+>g};~ghEQ6EYo6}40M zfs#2RiUE2WzEWa>m{{KU+;bZbui;&`s%(U1Y99-GBqyS81ZgkC9kcxIg#Lo*s>;J{ z6T<90q-}oAV;gGT;!LH&Ag2xKnU^>*!&ymGJRuqafCi&V4pSb7o*OReUh(BT`pHW7 z4SARjOcWi&;WYqEj5X5N=`JxYG0c3?p$%&5&Q&U{!ny$#b=IFDD>gJ5oofLtT-xLcFf-AS9M|7Ji^_#b9r{(+EDfPl8Z~ z%>kN6CHY^@P|Vg((DL@{7%<6Af3#miB)&)%Qt9Mu&7_QGxZOraS4SJdg;b>}#|X^X z<$H1_A6?pRP50dxH7IX6f!%Z}#D znFhDEjOtb?N!)U?=IjfmBl3CtN=~Hb z_`jcVkWb;?&tX+9%TRC1u|09cPhJwR!4*sU3Y}Uc=D(jg;fbH!|NUI67ykUepDX-q z{rj0usQu0M;CS=PHHyB(F;E{P#YH7b8gg$D-%pSp5P_AP{?6GoG0R97%8!0YvOcVx zGv7IpjAjP{E)rIT|SbS;@$iYSqf{Wt{i>xwonYIZC>p9Km;( zk>m5|tvW$Fq8^Qt8nr9f3*Vte7L%7i`uRN>XiA&M16DU}eP2dCV@TA*g4vp${cb4l zJo(L5_Y#xj;&vg!9dAhd*lm`yNOlYn1Y{7%-?4@EFMo#t)6QijWb-^TibXaLBbeMY zGoVgQ^G19T#Zx(gu%cFI?ZodCU5`ulI+3oCfhLJ&v1GGQ^<*r2a~~2DQxJ#{DO+Tv z#Nx*AB(VE$1SpMe;^t^i)6WbDM)o2#Q0?5v_`tZFnY|dFeS=kZ*KXkIF>Z51qyED0 z{0poCOY<`g7o(BBf+B49oOI>T^gVNSZ-fL*T z3{iCPne5pe{?k%hy#`;>{UQU8s_fS3d{mQicr`0`Bgd}t-i-{?7#Psn(Tk??FgV${?lo{uWb0A4tpPL&11*| zFH*`AHUYY9A|K--BR|P-V)IPD3u8K8O5*W4xY8N{%X$!W^R8=;KYlFqd0>9B;8Fhn z{k!poWTnHY4?XXXt*M{?y%!L;*MINz|MPeMaemJ+_XLNRj6CGSxHDuB&}c&j4IT1- zdg#9&%-unD@V@`|_dsGIN8`NVcfA?_zI$Gw3A`NEi(5JW7@+@HzJJ6Y>Joo z?|09+ye`+hH1xRzF{87-Nu$ckt8;~farpPUBgmi7naeQh$JapO-pHwN{~*0sqhCpD z`Hv0iR8n==NdT|7`P~%*v{bZD{qNtM|LG)8c$-I+FW3A}KmICo4Ob63?z;uT$;HqH z8r$0l8skq$T*azf9xHz_9`#USkU!4yuk{bIln;`Xd&f@M#pbTw03pJBOl^a`?+*ca zK_CqM^yfoU-8i^%ocI1+mDV#=@~8(g6iX4E%@O?8!Pj@FPwIKCY_li7*9#wj-hFJD z&TNm~uNSHGF=b31Gl8f-{7LZ3I_mTP5gvQSzOv4g4{r)Qfd z$)ub$mjWe?NXP}Sa_>a2#*x9=+IhKk?>hXz0%i%at-gvf#r*=V_5XCmLCRcEtqAY* zgd{BI@z&mDwlM>ZZE_KEWGL#Hzx`z4V*V5%F!{XQUmEuM$8=sDfOf6d3_drtY2Cl? z;D7TLeh8qK{qtC-aKasE33TxP?!)S;dApZR|50#Nh=xaqhX3w&^DT#9PhY`S`yY4K z{To+@KcWA#0tfm7Y1;V@VEurp;-6-iJzoA}%{Kqt$CwSS7?rpU5tUViE_l`?K34~f z8yxP#AEy@haKCw$Zh6317M^AP_D*0~8-J4Izx}Taa-i$Ial#6i2)w^RpYJXYz;e}- zD9ap*pYk5#);S(_#E!q5EFMwtU zQKvOuPummwE*P!dD(m&O=6C$xBR9Q0fYbRl_cO+BJ~{nqb}yK`@73&{p48LtWc_s6 zbZQ20tb8c7$*-U#zT|d`&;HOvNpgfH1p3~~AJ2MIlnGE09m?6+3Nj8!O3fKq`tos^ zuCnX5&li%n&hv+0gw8!r)O?%I`L(^W1~T)Arpps%4(4|-PC@E0z;z1VNFekw6`#F& z)D$Ys9w)HdNQWe|zAtg+Cd+@eZp9GX^j@oCd#-2}l+>5MJ|O(gL1CbKM3t-)8#gFl z(QIzX8)Y7LPs*T6IUJ4gc*tRaZ%?Pwb)U-D^dngcAC7`V*y-p_tGMlN;}80~^xT_2 zbjK89Exf`XO3JmXRQr62S3ZsQ6fn&JO2ji%{*0+L7F zzk-MIIjiY$U54-NShN>Gt69J~c^(=F!Br0JxjjVF*NB|cNqO>p^UKhtESli{S*u|5 zqZKY~jwT^V4{OdkvZ;eLK#2x6DXQxi+UjRbf1}ZU?>`T|{63&jZjY`8$a+!E0BHwr z=2XQ8-@1|$OnWR(y{{HYzBiWKQp&o{=QgjvjTGoeeVoE2S%;#rkJG&wtYUpg1D~x0 z6a$M^T_^NUj9?$3g|+!4-O(gluz&iR4PeI`;0u7S%}n)GP{yaxp1Q;HjcBOjDPr#k zV`4VAh~&A91`ttx*d_Rfy^V(FovW(eat=~Jmb<3eeVZ?(zyrDey@A*`sJHgS zX7TaN{GP|($K%uFP9MJ60G{+0=Ws@;_5Rt0xP@f&nZ$Ek>2)xkT_BZ_2&1mgl`|5< zE=ynpp{g`a&?y-B&%gE={^6c5o0{M7pg^z-QtIj_{c#%YWgj4NF~KI%wd#X7iDiNU z5$QtR?o6mlW-K4|ntZZscWt$$J5SQ#H@FY261;`=eFgv`)8}pE)A%Q4=&4}KO`pi2 ziyd;wrJmD5)zm!xt?j;57%GUDyVd7pjr-e>Qbu>2f^`JJBL&&^bUyStZTfv}U2ZJp zI7V&>gJ0uW>%I zV>m%_d^I$4nAUa33_CeKt@*??kx}kRl^d#{Ux$-7Da>~`2fV}(PBJAsVwxni^_AcC zS|(rho)5bY0T+ZT`TlJ{+StC7x7`rPGsAn==ylsCrf>f345yxTXij4W-b2s#-W%t& z1$<3TcRy3~o!?{@$uN(*XO;LyszP6i?f1Twsb|4;VR(%LI$LU9a ze)>cu(l^Tngm%rT(9+j`$8w<`k=g)hlA(rAs=} zXQCS9nON&rAm;l(pdXKRxLqEqH6GNs@y|Gh?_SMqk34S?Q}zMMz-IBm<@o$yy*%Nd7ZCxCPIjaF4&b5L$hG^8@!etfHV!|FPo9<=Y`gg|#P0h|9Lmuu z!3*b75cZ44AiURZRxW`)>m7%PFstH*>aKr-W9>H)Uva@lt?q@bx@i-i9_fd~z4v>xPwle6Ybiw|`~)?p zolNj@j$_!r(5*8vi|H>qIxmhqev(1E_|%o{9-Uy6iq&2?q=|SqrrmP>`^=+va86lo zDb*XUfx^ANB;hbnIs+uBq8Go?60D+- zM8c_Fgw$Mi1!b$_tlPO62oZY0>M<4Wk0FH6ghUpj5M@;lYd|M=pIxW_JR@vnqy=?y zP2KZvD`Z;FR2Lujpfv{l$7Mf=*c)ws>oTR4SL$RzUwcJluyZm~`F|lif|>fy%};K2 zd#+}Rh+4g^(udvloEX;p$HYxHQTmAbJd);Wrdm0xuN{ItTP3N0K}@g~yY-M7UugMe zMo{0IckAJ_rG3T9k%O-Hs?UFO0>1P^b+*00VhFeEY$GB_p2HHbUF;edO2>h zJs@p>V>0F%-`~a)ezc!O8mrnJc}JJC6tS0=>o~l$MMfUPiCxJ^n(a6{V`G*r1rZ;C`?T151yDK zqoYu3{JHyJ4o29asK-TPmSGvEM|fR8Gq3DT;$JU*lYaDWr)rZa0UEro1*G2|^n*Qz z1HdJEghPrQNa1v6-WzY({BFzU>7shr|ID;<{k|@1y?-USS&H6(pwjy2`O9># zUt12xyWsI15Vt%*N)8dnGg@+CJQRD;HvnzF_WH*@fj4yBw~8P3{F6>RS|1Na^K&S4 z&JJ-Nv{DSP%5I{^hey@y+Z(+KfgZXaETXby9t{SJyo))hHb(F>~@~V(pYb+jgQz;%29E{7pBr<+5Z?{Ij z1@zu(RVPy_@3EDPg&jjpsl%EdxlBTeM&$D_?d0j#5{%d;13a_cnts@by4LLoqo(Od z%8U_$0WjThSEeFKe$gd;mQ*-(i$D~oJh*3BA0hLPrRsN0q3_zcj`eGy+6!OB!-sBcbgc67NKZ5SEdB%EUefeoVp-bvgc}5 zrkb%8V(0z_rzmzVxR=y3MDO4IE;W@HZ9buu0t7hJ)Fu-PPAL67b=ws7@Thi+E z`r7c7BP_#u@0rJiNKr=LIzb;^-l0K`CB@xK{js?{bUB@iBrLTHJ-$lzz!|eLz;wbO zt&BM*ktw8?&Kl?T%6F+L7FrgnB4jIP8xF9ig+nXeh6a0iH{ul8qgzB z=xNc{2k^mjIwR&An1$mjrBl8-(m%-T9{lO9B8|Ql0imbOhhE<^F9_{DVsy|JX8^wPXS%!Zo#n%CrXV>b70P?f@X5ZV@8Q{xxj&y1%px#jd13Jr!B=?`>t?4fhJ zYGL%gw%_`6Xt90&yuMo+`N#XW+VCR>Ifmv$N8+f9W?*pPtU``(^C$gwXs=J^ z0??u~ZT0IE`5%dDla@ZVP6g&|^BZ@+SmQspGKsuz)%3d6sf>N7YaTM+>h3GP_4>SS zrLTM|edNC9ZyOm4KjjA{?NI}^t}qDq4C7z6eFR47(QTb0_ei&Zx%cOKupo5a_Fzlx z*=f>UBuK=iG#{36WWjI(kM?4)XatKlLZE2q05qL?qwj+LhjWZ>5>&$caFE7#F8`=# z7u;I25l|03oNc<6c~5WT>F}7lylcOE3T~!to~FHGG{~B6dn${D;oSOR2q%_w3TYK; zv1AETTi5HP>F?LCeE0zC#aI-BME8@q@}c{D0n#DY>qWW+nU<(q6Ieb+s^+WQBx$1J zIeT5H48Co?yWIz7keJope1xWXPyZ%_p8iRLQNe^8tGbg^>q98yMfqddfpf~&g6@BA zhPv~Q*)K{WgSz(nHI)9YYe@00QBU@GwgKj5p||omg*z1~0Q=ypejsK0@L1QhR0pl~ zoAus6S0>JK>-FbX*{+CfF{S$cdURHnm^x#zvJ$z~zBgtp!1~LM44_W)pD@h#pDQh$ zuwm&t%BpS*FciCrfy7rg>^!9nDLkkfa(O^j4UoHp$@nHYp8)~oSPZ^yt9u7M;B2^> zJ6_Ut?-IK5;?Vc^e1YPbUI5oAE5XCLs=D<&T~X3OkMjC+uzf)@_#pF4NFboN$?K!v zAR#9rFQ?P_R_HYtces3jY=bI85E`;D8 z5TcS_L+zfi)Vk9u7WGTY&sIYAgyEVcMr336<~|yH3rqWb}FA= zn3(312^SOu{{8+E*tT+G}D)wJWHUg8z z%sX7YJ0OE=?Ds8K$Vi<#x2DH(bNR5(NK3OZUns8^w2O1ZiRg(l2%-?%{0r#^xS6%E z!Dzy?ujRf&=jTkdKqH5fAy6z{-7|LtE53dfG6)#ypvBy!-6prb>-}%OeEc2`x1Y6j zen9V>yVble$)GQmU$LJ=LE2P2!w32U&=zx*8@uh5ap-GnL1%wfRNO3Us#(fq;_74a zMpgtnC0di-Pj1u)g)ZZ3}1nr z+VtfDNu^Esmhc-M*soy2j9Ndo$$1 z@}Yji#rC52S9PvLQXd9VgNUZ9jQtfKAuU^9&bo)-FBC=b0?aIGBu!D z1ea4O=iqn08t&Vu0Bm>-Lx)0xNBEkiLAWuO zH65jp1GV>jbY$<*JfJ%;a@EFCAU0Hmq z9ZATz?{M}_r_;ht{L{P9X0cQCt~(-olBx9c72rE=!YfYUp6Ev&Hv*c;MKeI{_st%xu!dEBv}erBtBZM=MI7S-SUn&R%Cld}Fq~RXZEu20>+*lawK?Uhi)_f*;LG$eI+!bag zBg4|Y!MK2pM)6{8+$*k73>(gUXNf{rzrTo^S(-OjWo(r|Mf%%5xL}rWBw_?Ia&MY7 z8l2x3`*+>!^%N_A?;-J_Is3)_zAN|0LSq{l?xVv3MmDV5y!p4k7x=rO$8lj^`TfOf zaEq9lXoT1cfyYNn)8`xpPM_^;4^OKIxl|Lx*n3> zBJ_$AsUgSwYVMfOXOU9Cu@0IoeN2YBz6vXAB$8~sv>XXx+ir)B`(0Dg@zaTvlY!(} zTSWV*ByZy#yR14eSzRdO1`Ad{TUQtFBACe)kD3USUJs`b$9L5TUNAFT(B>Fd%A>M< z6Ibb2gvfV>v>s__62{XA9sj?3K^H1;{iQA#xcRieMU;@J6^T9(8@wig(U<;xu>#X7 z!v;mO8U@~U4%hDAJ+|)=^|Y-=!F_JpVO`;C$?oZ_tR(h;T0>+=fID6J6p@1Xe(_oA zpM7CwY0&NnxM4<0hk0eTwresGEFfT~^33}JH?}*tr2xEkP<1QrpV5YW$zQ-)VW*wQ z(%W)wi4frz2`36G?4DgEl!s*zHJfF{r}+8CKLj8xh+=$)yKNH7R4;Y~f24eJCtfA^ zeFA}1*k`}Y3Bg=7Sd(wU4_TrV-=QAwysJkHjgt7S1jhe404>dRe~};5QB2tc0N2z2 zF+ptG`!y1fd_tH%7mtw6QC2M?R$U>YKeT4JeYtwKorzeW`Uo@&0_X#UoJAR&#XK)i$isND+==KX0uUub|W*cF41f|cbiST%;bR|lQvbR4k zt2=%OTgV#$+h4v>MtoM4iM!*kdF*s5h<2&cd}iuuu538@WUkyJoXflPg?t9}6#!h; zTi2Y&(oH6>CHv$S-iD52s&d~}V&1%zEU9e zyPiDb_W(27s{<4Ej`n%Bn+Laq)b?^DWhDY(Rp~N+pv9`hQ_lnfGpF+tIqY(ie}lss zj8u|gjyfxc$#C}swEm>moHa`E6cN>zd@t!XLOKtr7Y|dwvT}9Jh?qWJXs`AnBpjR( z=p%(MDlx&N$MSjOJGZzsfk1qhjOFt}xos!-qlrnN^>ZW&kW3OI(oo!i5~{U8xpoTm zedOJsGwGx28!Djd@x?0-BREHP*&_RQaGN!VrNQ*PcnjT}AdI(n=v*w2Sy>aMpH1x1 zHOS{)N08v?^Xg(tD@o2WA(Pd{kfZ#U&MhPJhp!xJVMqJ^*7(Q!ibYHST93^gmh?9o zz~H0-)anCGX~K7!)@XUJfAE1`h!|35h~%nlr=>Hb>$2>MOV4-E&uiXdiN7pkpNqqG zDbGa8VMwLWCne#vbw6YaON}8SEQ)aCv&tS%W>*>4a#fE~&gL!{O5+OT(VS&CiqXZ&I zsqBM zkNe%8n4kyG<$_+&T3L-cQI8Kzm2J(xDQM9mDUVF$)72m-$c`c^B8vjx z_=i7N@}idsQhEg)@ZWKr!U$MXmelEv?MS5JT_k2WKCe3xWh-sepnF)Zp^*fwK9l}_44+P5>Q@K=Jl> zzCK#z3i;ppQV@dn%wMu>Bhs(FNEC$#&TiQLUi-P3?+9l4xL!w{x^#wj+CfTiU9#Zt z?nvusE5kE-b+RpQV-4tJGsAZx==-}lj>ppu+%4jLY}D)cF^<-wKWn|vzh60b0lW77 zz4XbhpXMhjxg)@P|2{X~Qz*~B&37&$mY@*88snn#&aPiyK7t?xsw5ve80wk#A{;vI zgw!xZr*g=KNCk_Esr@BCk7ItkyT+}$o&e~(YP&m%O{Q1|cq9sc*x4QEbMvlV+GTMx z<>VW4*IdsNtOM%yc;LEymB3>WmpcjrS~9d~y%&BseS-ps&g2Y%^PajCA5MRN zKs~9bBQ=TVx8y1o(8EXE>*?s>F!n}69L0n3xL4W`Ok`Q;aqHhHVp1ww`eg4|#Se2DonPu8q z85uzca2)qJ$N0v<_?D3(Q;wop_AP~2gL*|6M;)tyUoL?yC!F_ZnIw4kfDqHgI2kzJ zQ)8bnP`3h4Vm>;Pf6t6Zzwd4$BE=Co<@};&&iL`h&x5l6j@LVY+oorE-v0XkPi~RS z>i#jSHKSjoiLpn!RM{WPU zhIt6zjO)AW^3EMIgdaTo)RkQOC@miok#e|J64=LWMmqH0D4fgw zKs|`f*4@JhO#Gcj?OD~%*1Gvg-M3rj+LupqMK|(b7#A2^u7aji5lhRS7U=XbJA6mz zq!O+kexJrhKG%2dPiW4%p?Q+2;qSS`QZu^i_8QgOy0G6{`J!gbhbD~=rvQ8Oa;U$g z59Yha&p{~gkg4&9pCf3y{3^dSwdf-nJT~YBn0Lj$`7r_aTr`w?Ys0$TowWQKPPL;4 z3&sxe;Xif07GGpe%oHqW^4GPM5mGVd-!)Ix&;bh^7qTob?Bywf03xVk*UnUQHHN=A z5+Qi^`FeT~;Mb();7EJ@SQZCQ&*r)}eaIgXa5`5*fDu% z1OM-@iMwyh|7ihDQb#cXd{76FL|?mh!~>M{&^9EygxyvT^jXzT&7T&v{4F{0u-8B2 zrXuSCh7#|%!JsBJ5a?9dK+TL&>Qw-N{^T)5rt-+qfqC=c(g-PZ`8P>*U~45%^F5uZ zgNjy@-V)jt_T%|0CH`%3#eBa(P%IpveRS?@tfOP2?LIE7k~Lv7?L0v_^z>V3b}8Jb|<1|yiu z*<2T|?&-*^C;Tohw|X0gh3M~Ut%d;A$|L>%()NB^nko??J(bl3zF5V_Zqc^76AfA9 zmDIGrZ01iv+u%_@QpovkL+gZ5h7>pIqNz$;TzgB@TtBrRfRv&vC!F?dKSRG5K$ipFTHSZa@IaV0#D0g{0+8U_ zwVH#4Vu@yZ$R5y+@POVApSN^FfkL==S{19+THctOb`Xe?J9FrZ1AECTH|5jHfLLBX zZSwgkY9T9ZOJ7j9&s(VJjY8k-HfohTKk9qCg1;KNIPMov|4XO`Jv+3`50A@u&PCZZB`cM zZ>!wRXaBUy(60YiKK}A4lJR5)4D zHRZr3|C7(}oWS{;36RF}KYHPE4_R~aIAQ@l|I>oceSAuD)NZqmy>85Yhq-7EGi!t? z@x;HcE_cUd1L`r71ds;+>~f1#4-y~c+OQ>*%HUiBB6n>X8(k^PuUC@#(znELS?UcGmsuSwZB)af9DS3Ae5 zs6UxMuPEsS|$PqguWR`C6~*`IR0$vBG-8A}9{gBMON@Xp)X06Ta|o=@ zhkV@0%sz9u%kyfSz2SDkhRwe`!izN8hje5miQF!lI& zLPBttypmcwo5GhMj9p{g8G$!Iw;)8m@O_fP4z?eyb#|cm(HJs_)5w;*uZ@b{iZ>!m z$6$X$v*Q?`=LL$n?H(BVpkzuU3COo7KJa{CZ+Dg^jqqUc85U`ardn1KFb&vP7!vjM zU1Qo{Od>?0?+mL1(b4;atiRuzA`sDqPBbk+aLv`xWOB(>2TD%zNTKtVc@|B zOGJ`8RARt7yU^vkIz~!;D;uX68I#*@mVR6FHRcKV(ZyZY zh1x$`UJxW^hTdXN`=s%Mo`Rm^Y3d?!(n%4Hr(!M+EVjnTm`wMLi3p^|V#rpw7jJQT zwa^@k4CrMfi!c(B9LaeS;vOubnU4LR{-Gr>(YN#C1g}H$r1oS&usC*VMKay8{t;H= zUbxID46jM5=6WOmGqdx&n>W)^CbOQD8w%3L`NU$w(kD_Ir8YyIE*J33p7(|%n~SRe zvMRl{U(J`J?pGp6(J8?s;egxNx&(hsRw;iJBo+43H;sHHW(Ywc(@T0`i|K3CQb=TI zWcBO2=3f@`1CqFlL}~E47%m=>1AAWGc>~Li`T=Htf5z zM2jIlQqtY#Ux{p^3)ziKV`e0(aNijAEjIpULD`6=7)d@s9GRCm&4jph*Il}6K(c8l zJZeKGFsWO61W?kZ=_`qQ%fM#7k+KU%+UeI2E<%WY`#pD2M)|RwmYum+G&>;l*ZbQT zUTs9qBGs4gtu7?8QR)YYrP^%wPyv|XbNiW4W=CVVV-qPy!sD7d z0;_21>UR9Qi)nqhB~N%N5zm30dsVo$?L?P@cwfcW621``aDGkoY-Hz-4r1x$_Fc-d zH~o>^DM|RNF#s9dw)8^Xndmy{u%pGca8$(-fneBxvX$KD8q?y4pI?r=5<{4nxTGIiE`eAfDgFs6kpWk+`%K6?qWIP< zbUq7?#K%H)|B9d{ivSr=7Q)w_d@I;b?Btdss4tFpk}f-HC{@;?4o_?Nczt2+$9M2* zrK}_!?R95Mi(jPu(-k;u=u?E2v;yG<-pkzmnX4dTTvkr@pI(uprwT*hM}P+N4s_VT zaR(7(X~4yi-t7v@Sr{s|Z9WZSwLb3M7_+Uqlo0*Il`+46H^;v4JYpq5lA9tLd9jBN zw5-^@*GG-Ze*I2aN(#Pm%?&8FcbJmSzw*dI$9BVt0RLtb!ttK+?C{>PbvVaAz)<>f z%YCpf-s8)Z%$BX)BeUagQHUU%qgMqhYRpIE012-X&zimI@#MRvw*6N|FIJwUmM9Qi zxR5TcHD|5)l|+psqY$KKpF7Df2*q}UDdR@a`&G4{jBo}FzT`m^ynNpguhIeU+! z+@s$jU60&o%!0wR-d+$Y>5EsgnY1v61R*in`&L3txdIYU7y@3|nw@+QdEEyIV~+J@#Hf`a(7XtoG9(ZIu#ZTX%a_ zp-(gV;vy8Kf8z?LI-jxzp6jK(AgrdrHTz9}*R1O{EiZ-dfmrrQLCSStTDUlgaEe-l zNy(q!Hsd;Oi}L{Guib_MUHPiD1ew;rO-4!OHrMCg>M;QnkNGSKkQm+5%X#IV9{<$_ zH}cJ+lq7V&>5zWR^=%2TT|22+PzdEdwBru1#QRVQl)&xv@>mYxjU#IHz#sqW=PvX$ z9lm!_j!wWTu=F3Xl_}msay~;D`I8)dtoL@{D&f7u_396Cdw*i(+)G*z%D=NU+}3a2lrRoucY4U9zlp z4ID^~c!5DYl*9UypSFA}n9JEHDr)WZQ)}MubIyM5s_x?=>t9l2L zn(c0UZuWe<>`k%RL5&{F{bBdHeA2_dRTk`MIc24S!!D)wgaWtTJuOcUg}ZC&PFwrr zu$$^nD!*H62_DKaYsdkqMzKIM2#Kcp^1-UmKg05e_L@cx9yL3Hg2!zda*&Jmo7> zEX_4H!sBt<<>jbli`Rj84_qB`pSf*5+o4V8F04?uW9+6DL*TvPtHcZr)^X}l4xP4* zp2hJpmpZMcq|36Y#IRq5tXWY2y#^kRAL!@lY+A%}A%|!u!rQu9q9`_7Rqq&$*>S-@ zV*NamH+>S=9Ze9aKhG8$`(DUO$J>_BJR$0>bQj^J_ICqIuHX3+$B8)vl0F1KI^7Ax z>kB}hO{7zUf4Wd2gC@zzm{o-WEux#|s2lWyE8O0wooCs5_q!uN|#<@nFZE`|PU3h!a6ZF zUic|EEN}J2h2M_%!>O5zuE*V@6y<4%RMLjAfS9JbAcW>e1<%uYTLmBWH?b%+K^Cmb zleS`ZI^C@_dKp01FG@VDuRW5<9Hg}GCGz*CLVCZCnc3zVFtDa5f0#sL@-M)M?kDFs zP;v>QdV0wx{wz;fqR21dnk?v*UCro-?SIo_CLqu?j@0!^wrsp5~H3`@s_75y%Z+D|KPp7 zLjn^nmi)h@_9Bk*~4@6*Oq>9cGd4&;4LXv z6I*opjx1B+oWr8BzJBfS4fZibKcF>O3YCjQkGXm%q>Gn4 zfqQT&aIIdvyNaETn3(u@+#}bBx89}4+Mg#Ca_xil-$jZ1I9I)#i}GiIuru|F!si0ENq)&Mzc4KH2B4 zZBdifAW`7_i?Q9%ZsUS?Pg4EgxexE-6f)`seEnrREON7t2?jWRhuSBX?GZepA)adi-f2)PK?uf{oMMD&v-mF|FbYpQmj z8I~6JzHrzn=6#d)e0Bkd%=VIaKXhbq2;=h{Z3Wyj$9nhrM9zzg`M7kAza^YI{Ng+l}ME3Rq({Oz;Pj_?~qua$0r^1@=ZL7S;$BK z&f{@l-!MNRl0x=DO`~g{o9d)NaXonE)uaE$ zh`SN_4e7@n^~Nd{_S?$Xw39e+Z;azv3yzq#+!h3RlZ?&Zl@iCta*Vx*xniZDXD!B+ zMBsBw8t(gSx8ErzzwGS)bv8FAS?O1NmOSLf8~+(cFlbzS)|@U9Ow50CMkr4?p*Xdm z(BMP`f1B>pSwC5%t1KXF4_5m3oQgL*r(a^@6(nUH01v{io^z&k zwmZ`A#6y=ql@S|v7jCj6JL6n0J3WwAlcYl(L<6ICyGc&+alG?3zeF{1>Y zgs|W|Xz#=>bDkltHI4C~{LMpjc5#2W$J^P7y;EKzzBp!s+E8fybh|d|`FY0NquB0! zcq6UBmS3M(u2Nq<-oHg$3^*259@q%t8h7IOu!7!U8U@9F-t{Q+8@KZ+JNQQHMBWbD zf>#$3R>S^!n54C^ksIGrF4)BSOg#!HGQy<6y_4Q$=uPaB)2SNi@k7KoVIL2fb$5VQ z4{dmbGrO;eQLudXuE*qNW)9y@cLJfi`+sxkUPj(u4o1kuFa{S{>+_xTwdxn-*8&0v~5nIBZKffMs%yVHY z$XXXG!A3^=@BN)oAs2aixensU5sG!&uB{uBj}_OGf8o6+U8xt#KYxOOSo2NIpG+RV zVk>^40lodr-EVY3<70gKJAR6LGl@HS`uxq^oUK$f9sa71IAOJg-%Z*(1Wgz=29y&s zWy<|%gE zyGw*iy{}#IdnMK*v{YEKA$bNwY!)^yq+|Y@&rDPBl(YDYzuLBUC4`png^cdu!CfFz zkQ^4P8>G{CSrPBy&O#tt6VA`=L^7xtFqem*Kk<%;5z*F#^VWXd_@qLjxy54m(Tara zM`0fV1WsveTB_~g%jl4YV~u67?HHWHR>#Z9dxfW);Ge$#KA*{ikKiJjfA5gpQX}sk za&Y+sBM+g_4-Y zlj!t@Y_K0Uu($S^wFPIouKZKZa?GQlCn5Rq?_2f;dh19zk7Kyee)Hiwk6V<#Z{vM7 z&JRUmKT7blTYK8&Pv;i4nZ2dfRr+Mv<$Y!AsV^?@NEmWQeZY6f!CWZssWUC3xLrGO zckU8(C~L4_L+Qvq;!^E^`uS zJ)#R`lj@5*4tmncgw0|qKWx&@IC++~^8V-zJ#9~*bcaNj0s21i0YVz5DDa=H+YP8WLS1UNImnKyCnx#@B2V^YZ7Oes1TtqTomLQO`rcUI` zA|y#f(&|0+?miWUdm3$0{sMFRx(RM0kh_er8Yz5A(#Fq2C?O{o$z(}nAwS|A9@(g` zP(SLiEMh5UW|BB>5~3jjhTlaIa^jrb-CS;|b0M-XA*{cKB(@={>#hU4S~_meiPsfs zp0N-JQi;zB0pwb2OGcK`NOvgSv&3%^I9A{1-Ib+;iiMt>!;}=;2-sCK5E1CpD2MD? z-1vz(nJprs-cl%a@(}bXZQjw^!d8z(Fdi%wmzOBAA~u}wb(jl8OoV!fogPlEXX|GK zsA;c>q+}-(aQh66Yb~&dgqwBedLS9neU27rXK~^VQc<6mCJEP zgcEQFEqVZOesvzo@Z=C;RVi#8vqe!k3*O34ZW2}*1VZGcX02rGo6B(NI=q-0(oHyV zXGt?v%&2Yd(}@7#l-~_{*nwpvynYMyGfdGC+-gbb9ion#^7TdcTdYL$N5Tsfn|MQS z7t3bQScLQxfs$CjShow7LOXkLzecge_ysF&o4HK(clNwE`#i;x^S{z8w=@WnLK#Oh zvINEm#ba~Vq@C3vz{RZU32-PSU6Gqfq6;n&h1UoT^CjPOip>PSL(T%3!;XYAiNzxP zYgpRB1NR;k0gsePmc8%`e~j12!zyyHg(tZ+bmBZ4#}DHq@aj87C1Q{0BeH39nwofm z@e0S#=i&TY&rkt)6JTl*t3QZ*6`9dHfYfzNiY-qOU~Ot?9Ji^0%nOJ%h+As5s}#fE zr}!gCX$|?Er*tsTcVt5kzZ%w&@Zu6nvdzqz9P|i98WTeoeH&#_5dK9`U|hUVG#D&X zECe5x30i!HjR(N>*1LP1Oq3=!TfgVlNgVf6M$R}kNi2sUW9SR3mrv>pdJ~7v|MBRl zOX#KTIJctTi-|H>tQ=0;hmps0W}^+D?SV0qXFM89yD)_bLx{nI8X?tFQ#_d~X96rT zrYwxB66=N`T!E4tkre>z7gS6hLqdYZA&$}r1zLpOc$fs~6Vf0J{cWu8kkXeL8}YUh zT*Axj29g^r0;zGcb%cNH>oRT6SY8de-~w3=Nh0wt zI${#;F8Xb)5;d<|CVJPxTJ8nC*T zY|Vg$5YgqvYZ!KGm|!;K2XRk(w8Ov?1u)KmfZmiZy`AzJNi*qi7l{wp_ArYo^tuzN zOhmCl`yNSA3{sL6t`bfb5!#%nYCH1YxsgSl`2OcTQs=MH>k+`yV~>`wa6Kk8E-fKf&l7Ad!Xr23i9Odqh zt^EJ*Yd}70PUsv78~S*myC#1+WN0elf4$WI*RM4?HV0#=um9M@0jQVmBq?I)Ew%;s z#h+;MPcuHZp9Dcx?SH=>Gk^MQlLKzprw`!Hni!F#OnuVQ`1iBejKwQ9C4eR)3c_ab zm$CpA7JtFJ`L8F8K37>lge0oxUHSZ5kG~<+uk9oxDV39n$-7MwLTy=_(K#?@ADye?IxaIxurj=^(FfAA@yg`Of>lea_SZa zKA1#Qd%3+Yg|~qk@xBcVe=xx!R_$n~f?~NpTw8TL&i4zb9QOXrDpTwALUg#^9-rUs zzKhH!MNT%D;ra@iSa>7&Uwz;MnOn_tH~lGrpHgz{1LhndaPwin41F`Rp+zsJ#fHYf z4$MF^1cu543gqYcZGOcrvCn$;tljiR> z7L%DV(eF2#A|WBzqh*`+$??cM(N8a*KSk2VP3#|w2so{&i&K_4uDsIshH5P6skPxf zS{_W;SO-Pr@;yHg;S05IiXdw%0)%XysKCGXKq6mR)V@DS<~(dGy&fTx4Se)>snWnb z15m@-BXA|sI`nqG!E-UEQvLgH8vk8I7CkgHQ2RgYR=U!DEIsKBndEZuzr8A)?&4L% zJz8N-l@u7hO+i_+z0}y-jePEXjA;;_gR&r5<^D*$KeJ_ z*#wtw(15%WTUW((a;}1Uj^HhDWQ-jK+3%+a-#cAy@G1PU%Kt1|Fn(v`PyTySz(MZ; zCs#SFe(ZnO_DcReYe?h)`}l@qyyWZdgpxRparxt&D@XSCK@PVB@1-jL0nza=oU#PR z{`a4F<=dz24P4+I4|DWihBBfD(E`X75_Bm0_QkWX`{SSgJtNWNwz-XA0NDZ+7=r2H z%Kqmu!n14hk|E|u492=wg1~i2H!^4ZYP_DQRg3UvQtZ6#bPVk$ddqKTI1`lczU`7E z9>-dC6=_ilvVs_tG>E9 zqAW`Jbnr#6RWf#~|Dq4QvX%J2)!D6WD8_^YWf@BYZwd9*3(ib7241(nMi5(lma;F5 zrFMonL#z|#b~ejzm}^G<`Mmqzd!hUKd;9u+DPFSuXfu*sh1PffCz=tke?csVCr z_P(s6T3;PLA&S-ioJQ{wAiI$Y^}=HG`XP>N%UEA@+VV1ft&dI0oga-gYqNK~(mT}h z^Gaf*))&d-voD+~`ObqaKYfEQ4 zIPMTtZxj4s&)Wd^njmpbqUW`S2wxuW(edq^SWv4vcw^X;yIXEtEi%9Sb1$X*_whSs zxPJH5k)rjGIE?}Cn?x`LSX>}PMJrC)V_9C;BFZ;XZEjDxg!a|U=ezEr%6}DR?mFw~4Awb5lkKrAr4=p?w48j`A zDJ#yrdL9NLSaZ@IZH{;&4&h14^P%ssuAXgp|&%8(a!PLpr4qsMjnaf6_}@+w@! zp`&l;pkQHF`y-G|kN%~}hZfPb=hHNCtbhnt9{TGshb}>-_m52714*hm#r0gJ&QI)m z2aAY%b<1-E$wU5q?OqjPP1zSBWwY_WJ+~>p9q#nLtnq<&w2E+K_o!h*25DB!nridIwY6(rt2~0O%!i<& z#&~*U;$*Ddw)67G+>uw>K2brx)}4HRxtA-?Fkh3WUN_mXpTmj;qJ4Te?|f{TpeQF4 z3)!n-<6(vWKKdHz#0a-V@F^HO9XmoK-sOr?5V!10`4Y^PUVAG)hwMKIq}WJ1pQcBZN5=YEjy z(p~8xST6_&x2}D8sb0PWoV;=HC2%h>Uj~rHjVzj^IEOz@4(Vya{$5L|m?9CtZ zkVwH!(!2AOUwc)Lf74C5El}f&r^1~UH}livDII+^7OufJ^T(P&_lS2L_o>DH>JSR& z=p^M%zW#3Yl2^6*`NN#}{2?gl$vO({v1(bC>az-bZ$+zBEy4bTVNn1bYoqLIrI0R1 zGw$dlJ76Z`m@#uxa{{$wxC1`xi>;~QUX__|r2G68)^}TppU)qpft}ksy$V&ESDGdR zxm|JoK~s{D(`|MHZK>qzcZk#uUB007TQzB$os&Jj>_NxGzAwKNM(U9*`K(%_l`WVs z`g_hXiwH+srQ)$X-Jqj21A}<@q6RM7p@pN7&klBVeth`kCx4q1b%>Orm|wD8mZQ(Ug+Y|n#~Vboqfh}`=~MZx zB;AY1KlT|8<)5e~>de)*uaEK~T-#oN3NI5>TmZZOl+cg~-S5yqbNi??+P7wJIPQob zX}^x;roGpbabnH9{k8+XGqrvB!JLBp@U)?VFK9k<+N<%MGF9Y@{3S)ijpWA@^ATO^1xjBfcl(kq05why+Ofai3{dG{MePW#+LxZZ>l6vmmFK(%AG{4m2_)+)FGe#A0I=;H|2@uc6 zC3vIb$5*phr@Q)fnhkcDMt1+r;lw|z?_nRQN$v)1|I7U|;PEG;{nX)A|Ff2{pImi{W(2RDyJ*Np)WbV}G2bRshW$8?-Zr{uLULXHh#iX?K7jUtD%A0O; z5NhPIymO*{NBmmGf|M!w>b0-!jm?KibiyFbZ~RUs*AfjNQ(a%Ql*2mrUT&YW+u18h zM|LV$=5{W{74SM#a&6M#nZfcWJc-ib{nd@slovKo6Y94LKhy_AMph;b$LVOLR6oUTJYFCQCY_gra;jki>^Z2ec1;ga5)Gc(TEw{9>4hFLFw=T3(= zd=}Tl0_JZVfP4nw*7w@Ex$wJVym99*NPzI%;Q8kl!Fk-OJ?wZV`_8s+taJ9)Z*{si zm+$8Zf@UZ(gDU@ditL%+5HLx@3iv7E%$6C&OBqXlO`49oJzgCPVWC=vqh zr*49zDP#*}X2%HSxl^nEenH_pmD|yIDTVAP&|@N~Jun#Vb%4`RwD{8;>S`%0ZRgZ}4aaV=gK? z`s=>mfh3)M@j7Yu^O>&ap#v2}EcY($PST9Ze(Rq~7C#k?x{qs_|1JcNPB0Zu z6a&D=sr=p_nbfUvXXDjmQ0MaW!@FJBoIP{|s8-2kz;=7tw1V8LkMC=C{KEly;Hcm0 zw-~GCP>1T_UdZ>$F9A{JO@M>z39tQOiTyS}j)3_d#F|y9t!|PjxEA zNG}ggBJNA}O&jQ?b|rsZSy}f!9FzHsUf!-gp;6x6Cn$sf0HJ>ZL%MI#kSOW*^ns4x zv`6@0kt$c?=e&L$Fqi5P4K1{Toj0>D0e&ChY$Xx7aHMK%S=}dU_LnaF4C;_`@bPZ= z29UF=lj`|rYNV83!i-L}g8LcL_VA)B$cv}jk|by?6X%N>yvm=t1$ljut#kV$%K0WD zh?~>ea)5f^e(R6aTSJ^f%<8v`H9=7?MV>;!^MzjC*P4u5Jt;GsBhT$jNWj z#wYSB_XG3>+5=?+bfNE&lU705A9C@Mz^13Dy7Km}+eN`9u`gEfqL!Gf%M2;wO{>{8 z{ib(@Rz67G^66jT%77@^l`EaA&MI}8h}oJ+O(}@>hx_@!fn%(?kJ5 z2aznZ$Ed*POq_9u9aZRBtU1)qj!nbzM^2t9xyi>*OKG}hJy!Plh^vU{c?wy9|Z(w zY~BA_P|}>dpHAmvxm-EvFGyiJ#E&va3u%FL8pVaXF7?)QD-7m=uLP4NR^qp7zP^@B>S?#^WVGB63|GO<{29(Q6(};+Lhq@vn2?HQ zvCAG3%h@h*Xo=*%?{4NhB3kBr<{s8Lpuf!!AwaP3AAQzxY?+d^wY*p9cqXDwE^Nco zysYh<+kAxv2H)mthspm$ud0$o%eQ-~$v2 z+eUpF^|s6R`SzOup&oiK4{X(Rros8tBgK&@07F?x%6wWz1!_aoaA|ByQX;j9xMjRhTd%8qTHtKeX=_5e|dG$%IA}Tz3W@w4k5C`jm=`N z2|w+B8j7uI)tZ^day8e1n12BSfkygn+G~HWXzJ7f@_JX>1`0GJH!f(z5hE#W0qlLCM^hbZKBm_YB7OHn^cLDT8rKqS^Ng+I@KJdYs>IKe8XTZ>x49ikDU>vqNKQ8>~$s)7xzPo#g?*Buk& z5q@1)4zx;Gjt&_32Kr{?uew;#+|6gOj1H5duoJnyxk2B+f`8~+KVtK7|aPwQW# z2g1B7q98u7UZH-kfNuPK@RkXKCV0k(Y4>$XBxlV$=#;=~BH&1#@14ZWD@*qK%O`o_n82B z1SB zvf3QHqu)~0fseKv=9pu3d9?qo)x4@<<9MF+CtmhC#LbIpT2C^h@ta3`z?8KPF3!vl z^_14C^6Q!glu~qk+d9W|+(d?z>JHq~eVco{g2GkBIb?j@A&k#=1)-CEXdxje57)pI zS5JUD+k9NEjbS~Lu@%PpE?qud0&2Apu@?3LTK94W4J0CZZhIJwh^@Jh~&7Bi8pLRh9A*P3lk7GI!tM_6k{~ zaOuO}vV_7a2=oD*^R`VEaAu?I`-n7zq%USSdmK7~l0wSZNDT3V{iq2xHTGI~X%Kj* zsIkYb5F?8&EG6c`fcXPRHWhfMuI?thB}KE|Qz(>pw=;n159`iAn|LyG+IS$sL3 zB`0z(CsKL>33%vmnwtMcWe^_leBcdOj!DA&ziK;|9!ItH4DU=LMUiq&luRieK*NbI5U)wF4m5YCb*oUfb%1&5 zyK!tKZeHipDX_Za0u0r42$uFLJQRoZcG>eS#DAb^&=m^r#@l{0^Y+dj+}HATjT;mn zNs93B|Lbzl;DqLA?P!;Ix9g$%Xh{{$3uL@I;0@J%w0c@D*{`A0$ptH(&pB=)C_W-r zgc=v#V}*6qb*jC45Bfi4`2xIfi+kQub<6flBvXCE=%_@|l2shK!JM`Ck0Ry`^d2#? zCO+}Zu&V=Sn)!^`iPdzFAiK&tU*+FM0UaGdnGLlg?ywz!9eQ1z_-B(I)+7W=Bj|D( zf4FKZbGz!~T(hLrky_b7zTS(+Qh+!sCmCs44 zCxydqA&N&Ucdt}T6z9%~Fas>C_L2Yx3j~=ZDYUW~j&X6gVBwa%T$nJ~&&I5C^Ocyq z24{`FHWvz;M_gV(I=lEu8j!;^?ho5ywd4HKy!5nuQM89EATr%5d{si zzCcqxiGZz3eeC84_8^M$znL6fg)?~}?%{+v>PYtT0(g*%4LsP$Ell@aULs@Bw#dq~ zxx=4-pUVL#8CN);Yv|&2R2@MXbp`bWT5Fg-(_$WdNnnw0c^fQ$bkYqrxoeb3M4cQ_ zhg$cLqewscah6La@qXtcabBl6oSiW79>odp`n^ zIx(86%t>n3+t7WaTj2>Vj?UnM5Xr#5kbM00nYV|4{#uy?)>A8D9l=v#3H(k z)am}-o8mq6Z&DiEMbjn(nN1LHI$XwUSHuON>c*i*p(LQJ;XYciw=ER2jx6E0rAq{6Tr$CfcIuNmt z7?cMREQSCv8t^itRvur6>ahu{jOUaMMV)4e@Z1zFG`(EGA*X@*Ct(`vEcub%P^T;! z?iaL91m46H33|CU3Y(BA*bns4UfVa)#S|YNc(vpptvp|E`Hj#qLd})+HI`sTykhM^ zrDerI|JHD<0xugpeb_VTVrI4Vw%1l1boasM`V6p;w^FptrQr?>h}mC!sl0DXTMpIF zKWLi*;PT$SgVKJfS}floclYKEo2Nwl*@x4XAJ~Cfi?8v9PC;bUoHt8z)ngBr`6ujl z%~Nnc182+6^Wn$)%E^yRP)x<+DqoSR&TZKo_qsO!WZnt~%|~L;Lu93XRPAeLVVX7U z?n;-Nzh&ia;2s_YoR?8ftV#2U^F*m&o5sDy=-F*-TWcjw#kE2x1_?^12m*tQ>u+J> zVUd7^^lRRT&yge4h9CFO%pS0vI6*)6)ij}Ozzz{aU4pwQGxlLj5$l`A4`cIMi?Mor zKRM=3PsncVIqv3Z$nrs3`Xeu8D$Kmzx;lbjzK9zluKD>qB#)Q_j|G|nH$2eKj=u_s zM&DYNpY)q`epaV34O|tEN&KyEofWImomz-!&^BF!$8-jQnXd8}Fd$e=_O3b_;i;e? zu;6bik>855lA3z5v#{_i?_ALCeNt6-$(W!SJMP5BWV4P5a>oTD78*e7NSu%9BK5_) zz3fBkewJYQdR*;g%dtM{y#2QEU&?i#znZ6D%nB;2RGO*lX5$~k097}h)uN;4P5Hj% zOxHPObo^3udXH$MHJ#sJIHYYaK1?#L817V@E4FrYe{Vl2Dpa#b3s;xSdC!OGmI4Y2 zUDi=xs|V|K#+bKs0w++ky7ML0K$1(vhOgCGJM69(4G@&ZzXjLf{?hx}8PC>ZjfmRo zbU33K)bOR7hIyo-32sPYJLIRln`Z{`z9H(@zuqZG;SyCz9 zm>g7#!yD6<`4jI~ZYMgv*XjVZN*Q});yq;^)T?FxKJW)&PSs>Bf*H6mbXGqR3+M!A z9y)rkq&=d}#m-^joU3}%!hj-!k|CqZ>@%|==lC^(rlK!5r5&w8-wq0mf4e$m1af;4 z3P2N0gajM0INJfH{JWY4I}*jye8niYn`-66@KjbM9$y_Xt(TAe-suc#)g1HWF1WyH zr|~O>WX&zbW3ZH>N{h^lUmZ55L2vqr@&=tTZn(@e9XKNn z3wC|YxqWR=!E9=RHOPp!xvnOtImyqqeOBsS5Nw%wbihFv1hJnOyDUc(?!)BON`&<5@`wVK78(P)O@G>W5g{^`e?f#qB4?9O8q2r zAuh~k(9q!hE`peD4c5U1VJVG7!j-{SE&?azm=U@%E3(X~^%!=V#ir>_# z5Vxx>f9l!!POn2)Ex@L==^tYY=E$Z9Z~{Vni3)ZJG2j!R#lsc3b-tuVAToJ&KHH#f zxNC9YzpxyF`@jo+jBNmVuYlf--;Wb^IXP|!27%&|**Vi1Huy5tIFr?Gd)K-(B7V+h zKr?Pu;ViISNh5Nvy?5#j)TcB@u~{co+=d5dO%!=P^P$u1+uS}1>B)6fJTkT~YD7ga z^jE@Ve>%Sv)LqDN1Y_cS+30qJCT`gW6a*`K8tf&!+}@evMySqypaC%n6Zgo>4fdhN zK3Fr~wA1FSsoJ<%M6QkYiqS@iLc;ex8L$TP;&>PT9BWA{N$;AUuG+bcx!8Xb56BY# z3~Q;>S%hZ%U9Z7A92}408JH>uXN3dpSaRYq>XuSB-i zYDI#UW*(yqe%1}{#Kiu6*+sFzKJGbzfwez&Y9|OsYj_nEsShy9-l4DvynWQT4j2m6uu&(dg8(4kpOzzN2&ir@hE{VN6w zCYVb&sOSlUTa?A?bd ztTqC7;8FEp_|LgjIdfVI(O#(L&$TBBn)9}qfa?#?@Znsk&|AMx(ar-C!snW*pY+i@ zlCKGw=NXTYK+m42N8r1iMY>QPT^Lc|E7{w$l`9|5l+Srnr>L-n_L3s^Ow!5yW)X@M zh1-M-&EXHmQl6$@EV;S>l$y5-Xj+63$CY$}++$9;bwY$}?a#3fcX73GPS=aP*Be)w zKKrVgwzji-?UAG1f3DH<1!bEm&mJn0TVYhWBj`5L5Kfgqg;TP6?`YD$x*JYL#CC#| zGdnfjq;@!;RpdPrW=nJ;78@B}x-Btu88R76ZtZuBrT0P%4DyDI9LnbL<76X+VMv^v z6~^^MG|Szqo$LbwC}Fn>?Q0=txWkV3p&WJ=QX^opl;x=)T&1>37KTW7cehq4+3=O5@N9MB{gqEgq_me_8~wO63yF$$-Z#LTlijr;>^>QXBK5EU{6)BvG&z6S?P z_coe+*!125?4!z)nDB`b`P?QF|FEkPP!(y^pB|~O<_?Uci+d2PKuHf8UjLjbWjeLy z{I)>g{)LCMj?e$YLvrbMP^`G7<8txP7--tL9jvwtVb)&Y3K&kBQ*mdPnT^WyB)Rt3ox@52zw%Rd4 zBCrTq`QjKERxqgE;Ox`VbQ;Tl`*HM!+0;tKma5`eYo8)}N5x)DhFF|rRq3N1tXaZZu9E=h7yC;4mB}_9N(<&f0`y1X(LF+JvUDMjlQJ>p zoGJ@*K5xK$JDA~FB5*=|j`y(XPdbj@ad^yV5e7gess|ibK!cvj2aFXg86EqKQbG~0 z4gB%8$FvK*lRJ6a*SVfQyuNdt-V6q61ZX$(OGN}si(Nt_@VYLT2&yUn6?#vD6v|F;>cjhIr>RGIo@l7jQ@F;j8wS`}Zm3&89CFL{JAxq`9|5MF7qN&q)F+*dWWt zQ1!6^#zd~Q+06m^DPCawl>X^YU;V#*UBCa$x2*kE^v^E)7Iszf_3z*R@c9f(23`NU z!bz5V{m1ve{pD-lvNBGlZ(aC&&R_rf_5JU^p{F>D`{e(A;Sb-6>X}6S^VhHZ^5Qmo zB|krtG>qn-jq3UHH+=Z|{+IZOF66KO{Q74HFZ-wOfA@Law>pb*eD{#NFx)7=_J8sH zkH7zX9CqQ?_kY~rkEFn8wn}dUn?bCwbuumA?C!a!*}wjV`=8qHfA!g*ZzT{ Date: Thu, 22 Feb 2024 10:27:29 +0000 Subject: [PATCH 020/121] Fix funnel range used in correction calculation. [closes #246] --- python/BioSimSpace/Metadynamics/CollectiveVariable/_funnel.py | 2 +- .../Exscientia/Metadynamics/CollectiveVariable/_funnel.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/Metadynamics/CollectiveVariable/_funnel.py b/python/BioSimSpace/Metadynamics/CollectiveVariable/_funnel.py index 7c8e0ff01..ee73ca15f 100644 --- a/python/BioSimSpace/Metadynamics/CollectiveVariable/_funnel.py +++ b/python/BioSimSpace/Metadynamics/CollectiveVariable/_funnel.py @@ -592,7 +592,7 @@ def getCorrection( volume = _Volume(_math.pi * result, "nanometers cubed") # Estimate the average area of the restraint (in Angstrom squared). - area = (volume / proj_max).angstroms2() + area = (volume / (proj_max - proj_min)).angstroms2() # Compute the correction. (1/1660 A-3 is the standard concentration.) correction = _Energy(_math.log((area / 1660).value()), "kt") diff --git a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_funnel.py b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_funnel.py index 7c8e0ff01..ee73ca15f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_funnel.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Metadynamics/CollectiveVariable/_funnel.py @@ -592,7 +592,7 @@ def getCorrection( volume = _Volume(_math.pi * result, "nanometers cubed") # Estimate the average area of the restraint (in Angstrom squared). - area = (volume / proj_max).angstroms2() + area = (volume / (proj_max - proj_min)).angstroms2() # Compute the correction. (1/1660 A-3 is the standard concentration.) correction = _Energy(_math.log((area / 1660).value()), "kt") From c2aaf0b92a18840fb1b34c76fc67ccf4f969a1ee Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 22 Feb 2024 10:36:29 +0000 Subject: [PATCH 021/121] Change file extensions for interchange files to Sire defaults. [closes #240] --- .../BioSimSpace/Parameters/_Protocol/_openforcefield.py | 8 ++++---- .../Exscientia/Parameters/_Protocol/_openforcefield.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py index bd86f5371..2f6358893 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py @@ -329,8 +329,8 @@ def run(self, molecule, work_dir=None, queue=None): # Export AMBER format files. try: - interchange.to_prmtop(prefix + "interchange.prmtop") - interchange.to_inpcrd(prefix + "interchange.inpcrd") + interchange.to_prmtop(prefix + "interchange.prm7") + interchange.to_inpcrd(prefix + "interchange.rst7") except Exception as e: msg = "Unable to write Interchange object to AMBER format!" if _isVerbose(): @@ -342,13 +342,13 @@ def run(self, molecule, work_dir=None, queue=None): # Load the parameterised molecule. (This could be a system of molecules.) try: par_mol = _IO.readMolecules( - [prefix + "interchange.prmtop", prefix + "interchange.inpcrd"] + [prefix + "interchange.prm7", prefix + "interchange.rst7"] ) # Extract single molecules. if par_mol.nMolecules() == 1: par_mol = par_mol.getMolecules()[0] except Exception as e: - msg = "Failed to read molecule from: 'interchange.prmtop', 'interchange.inpcrd'" + msg = "Failed to read molecule from: 'interchange.prm7', 'interchange.rst7'" if _isVerbose(): msg += ": " + getattr(e, "message", repr(e)) raise IOError(msg) from e diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py index bd86f5371..2f6358893 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py @@ -329,8 +329,8 @@ def run(self, molecule, work_dir=None, queue=None): # Export AMBER format files. try: - interchange.to_prmtop(prefix + "interchange.prmtop") - interchange.to_inpcrd(prefix + "interchange.inpcrd") + interchange.to_prmtop(prefix + "interchange.prm7") + interchange.to_inpcrd(prefix + "interchange.rst7") except Exception as e: msg = "Unable to write Interchange object to AMBER format!" if _isVerbose(): @@ -342,13 +342,13 @@ def run(self, molecule, work_dir=None, queue=None): # Load the parameterised molecule. (This could be a system of molecules.) try: par_mol = _IO.readMolecules( - [prefix + "interchange.prmtop", prefix + "interchange.inpcrd"] + [prefix + "interchange.prm7", prefix + "interchange.rst7"] ) # Extract single molecules. if par_mol.nMolecules() == 1: par_mol = par_mol.getMolecules()[0] except Exception as e: - msg = "Failed to read molecule from: 'interchange.prmtop', 'interchange.inpcrd'" + msg = "Failed to read molecule from: 'interchange.prm7', 'interchange.rst7'" if _isVerbose(): msg += ": " + getattr(e, "message", repr(e)) raise IOError(msg) from e From 50b5bb6092bc61b4204e8b3b918b4e120251ea65 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 22 Feb 2024 13:30:29 +0000 Subject: [PATCH 022/121] Recover missing molecule0 and molecule1 properties. [closes #248] --- .../Exscientia/_SireWrappers/_molecule.py | 28 ++++--------------- python/BioSimSpace/_SireWrappers/_molecule.py | 28 ++++--------------- 2 files changed, 12 insertions(+), 44 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py index 80ed3de08..7da921f96 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py @@ -80,13 +80,16 @@ def __init__(self, molecule): if isinstance(molecule, _SireMol._Mol.Molecule): super().__init__(molecule) if self._sire_object.hasProperty("is_perturbable"): - self._convertFromMergedMolecule() + # Flag that the molecule is perturbable. + self._is_perturbable = True + + # Extract the end states. if molecule.hasProperty("molecule0"): - self._molecule = Molecule(molecule.property("molecule0")) + self._molecule0 = Molecule(molecule.property("molecule0")) else: self._molecule0, _ = self._extractMolecule() if molecule.hasProperty("molecule1"): - self._molecule = Molecule(molecule.property("molecule1")) + self._molecule1 = Molecule(molecule.property("molecule1")) else: self._molecule1, _ = self._extractMolecule(is_lambda1=True) @@ -1565,25 +1568,6 @@ def _getPropertyMap1(self): return property_map - def _convertFromMergedMolecule(self): - """Convert from a merged molecule.""" - - # Extract the components of the merged molecule. - try: - mol0 = self._sire_object.property("molecule0") - mol1 = self._sire_object.property("molecule1") - except: - raise _IncompatibleError( - "The merged molecule doesn't have the required properties!" - ) - - # Store the components. - self._molecule0 = Molecule(mol0) - self._molecule1 = Molecule(mol1) - - # Flag that the molecule is perturbable. - self._is_perturbable = True - def _fixCharge(self, property_map={}): """ Make the molecular charge an integer value. diff --git a/python/BioSimSpace/_SireWrappers/_molecule.py b/python/BioSimSpace/_SireWrappers/_molecule.py index 02b662c14..44af7619c 100644 --- a/python/BioSimSpace/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/_SireWrappers/_molecule.py @@ -80,13 +80,16 @@ def __init__(self, molecule): if isinstance(molecule, _SireMol._Mol.Molecule): super().__init__(molecule) if self._sire_object.hasProperty("is_perturbable"): - self._convertFromMergedMolecule() + # Flag that the molecule is perturbable. + self._is_perturbable = True + + # Extract the end states. if molecule.hasProperty("molecule0"): - self._molecule = Molecule(molecule.property("molecule0")) + self._molecule0 = Molecule(molecule.property("molecule0")) else: self._molecule0, _ = self._extractMolecule() if molecule.hasProperty("molecule1"): - self._molecule = Molecule(molecule.property("molecule1")) + self._molecule1 = Molecule(molecule.property("molecule1")) else: self._molecule1, _ = self._extractMolecule(is_lambda1=True) @@ -1521,25 +1524,6 @@ def _getPropertyMap1(self): return property_map - def _convertFromMergedMolecule(self): - """Convert from a merged molecule.""" - - # Extract the components of the merged molecule. - try: - mol0 = self._sire_object.property("molecule0") - mol1 = self._sire_object.property("molecule1") - except: - raise _IncompatibleError( - "The merged molecule doesn't have the required properties!" - ) - - # Store the components. - self._molecule0 = Molecule(mol0) - self._molecule1 = Molecule(mol1) - - # Flag that the molecule is perturbable. - self._is_perturbable = True - def _fixCharge(self, property_map={}): """ Make the molecular charge an integer value. From 6e106dde1c39df31dd339e92b18b7d3eca44fbb6 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 26 Feb 2024 09:37:15 +0000 Subject: [PATCH 023/121] Improve error mesage when attempting to extract all dummy selection. [closes #250] [ci skip] --- .../Sandpit/Exscientia/_SireWrappers/_molecule.py | 6 +++++- python/BioSimSpace/_SireWrappers/_molecule.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py index 7da921f96..6de6095dd 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py @@ -1807,7 +1807,11 @@ def _extractMolecule(self, property_map={}, is_lambda1=False): try: search_result = mol.search(query, property_map) except: - search_result = [] + msg = "All atoms in the selection are dummies. Unable to extract." + if _isVerbose(): + raise _IncompatibleError(msg) from e + else: + raise _IncompatibleError(msg) from None # If there are no dummies, then simply return this molecule. if len(search_result) == mol.nAtoms(): diff --git a/python/BioSimSpace/_SireWrappers/_molecule.py b/python/BioSimSpace/_SireWrappers/_molecule.py index 44af7619c..089c88146 100644 --- a/python/BioSimSpace/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/_SireWrappers/_molecule.py @@ -1739,7 +1739,11 @@ def _extractMolecule(self, property_map={}, is_lambda1=False): try: search_result = mol.search(query, property_map) except: - search_result = [] + msg = "All atoms in the selection are dummies. Unable to extract." + if _isVerbose(): + raise _IncompatibleError(msg) from e + else: + raise _IncompatibleError(msg) from None # If there are no dummies, then simply return this molecule. if len(search_result) == mol.nAtoms(): From 9ee3d7db7197c5e84f3e340ff7715c5e96ba5c2b Mon Sep 17 00:00:00 2001 From: finlayclark Date: Mon, 26 Feb 2024 19:21:54 +0000 Subject: [PATCH 024/121] Avoid setting all end-state properties when decoupling This addresses https://github.com/OpenBioSim/biosimspace/issues/252. decouple() no longer sets LJ1, element1, or ambertype1, rather this is done by the _to_pert_file method of Process.Somd. The related tests of "decouple" have been removed, and direct tests on the SOMD pert files have been added. --- .../Sandpit/Exscientia/Align/_decouple.py | 36 +++-------- .../Sandpit/Exscientia/Process/_somd.py | 59 +++++++++++++++++++ .../Sandpit/Exscientia/Align/test_decouple.py | 12 ++-- .../FreeEnergy/test_alchemical_free_energy.py | 32 +++++++++- 4 files changed, 101 insertions(+), 38 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Align/_decouple.py b/python/BioSimSpace/Sandpit/Exscientia/Align/_decouple.py index 1ab4ca7c7..5a953632a 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Align/_decouple.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Align/_decouple.py @@ -76,7 +76,7 @@ def decouple( if not isinstance(value, bool): raise ValueError(f"{value} in {name} must be bool.") - # Change names of charge and LJ tuples to avoid clashes with properties + # Change names of charge and LJ tuples to avoid clashes with properties. charge_tuple = charge LJ_tuple = LJ @@ -86,7 +86,7 @@ def decouple( # Invert the user property mappings. inv_property_map = {v: k for k, v in property_map.items()} - # Create a copy of this molecule and Sire object to check properties + # Create a copy of this molecule and Sire object to check properties. mol = _Molecule(molecule) mol_sire = mol._sire_object @@ -97,7 +97,7 @@ def decouple( element = inv_property_map.get("element", "element") ambertype = inv_property_map.get("ambertype", "ambertype") - # Check for missing information + # Check for missing information. if not mol_sire.hasProperty(ff): raise _IncompatibleError("Cannot determine 'forcefield' of 'molecule'!") if not mol_sire.hasProperty(LJ): @@ -107,7 +107,7 @@ def decouple( if not mol_sire.hasProperty(element): raise _IncompatibleError("Cannot determine elements in molecule") - # Check for ambertype property (optional) + # Check for ambertype property (optional). has_ambertype = True if not mol_sire.hasProperty(ambertype): has_ambertype = False @@ -115,10 +115,10 @@ def decouple( if not isinstance(intramol, bool): raise TypeError("'intramol' must be of type 'bool'") - # Edit the molecule + # Edit the molecule. mol_edit = mol_sire.edit() - # Create dictionary to store charge and LJ tuples + # Create dictionary to store charge and LJ tuples. mol_edit.setProperty( "decouple", {"charge": charge_tuple, "LJ": LJ_tuple, "intramol": intramol} ) @@ -126,35 +126,13 @@ def decouple( # Set the "forcefield0" property. mol_edit.setProperty("forcefield0", molecule._sire_object.property(ff)) - # Set starting properties based on fully-interacting molecule + # Set starting properties based on fully-interacting molecule. mol_edit.setProperty("charge0", molecule._sire_object.property(charge)) mol_edit.setProperty("LJ0", molecule._sire_object.property(LJ)) mol_edit.setProperty("element0", molecule._sire_object.property(element)) if has_ambertype: mol_edit.setProperty("ambertype0", molecule._sire_object.property(ambertype)) - # Set final charges and LJ terms to 0, elements to "X" and (if required) ambertypes to du - for atom in mol_sire.atoms(): - mol_edit = ( - mol_edit.atom(atom.index()) - .setProperty("charge1", 0 * _SireUnits.e_charge) - .molecule() - ) - mol_edit = ( - mol_edit.atom(atom.index()) - .setProperty("LJ1", _SireMM.LJParameter()) - .molecule() - ) - mol_edit = ( - mol_edit.atom(atom.index()) - .setProperty("element1", _SireMol.Element(0)) - .molecule() - ) - if has_ambertype: - mol_edit = ( - mol_edit.atom(atom.index()).setProperty("ambertype1", "du").molecule() - ) - mol_edit.setProperty("annihilated", _SireBase.wrap(intramol)) # Flag that this molecule is decoupled. diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py index bc5e13c74..dc0ac4028 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py @@ -43,6 +43,7 @@ from sire.legacy import IO as _SireIO from sire.legacy import MM as _SireMM from sire.legacy import Mol as _SireMol +from sire.legacy import Units as _SireUnits from .. import _isVerbose from .._Exceptions import IncompatibleError as _IncompatibleError @@ -1001,6 +1002,9 @@ def _to_pert_file( if not isinstance(perturbation_type, str): raise TypeError("'perturbation_type' must be of type 'str'") + if not isinstance(property_map, dict): + raise TypeError("'property_map' must be of type 'dict'") + # Convert to lower case and strip whitespace. perturbation_type = perturbation_type.lower().replace(" ", "") @@ -1027,6 +1031,61 @@ def _to_pert_file( # Extract and copy the Sire molecule. mol = molecule._sire_object.__deepcopy__() + # If the molecule is decoupled (for an ABFE calculation), then we need to + # set the end-state properties of the molecule. + if molecule.isDecoupled(): + + # Invert the user property mappings. + inv_property_map = {v: k for k, v in property_map.items()} + + # Get required properties. + lj = inv_property_map.get("LJ", "LJ") + charge = inv_property_map.get("charge", "charge") + element = inv_property_map.get("element", "element") + ambertype = inv_property_map.get("ambertype", "ambertype") + + # Check for missing information. + if not mol.hasProperty(lj): + raise _IncompatibleError("Cannot determine LJ terms for molecule") + if not mol.hasProperty(charge): + raise _IncompatibleError("Cannot determine charges for molecule") + if not mol.hasProperty(element): + raise _IncompatibleError("Cannot determine elements in molecule") + + # Check for ambertype property. + has_ambertype = True + if not mol.hasProperty(ambertype): + has_ambertype = False + + mol_edit = mol.edit() + + # Set final charges and LJ terms to 0, elements to "X" and (if required) ambertypes to du + for atom in mol.atoms(): + mol_edit = ( + mol_edit.atom(atom.index()) + .setProperty("charge1", 0 * _SireUnits.e_charge) + .molecule() + ) + mol_edit = ( + mol_edit.atom(atom.index()) + .setProperty("LJ1", _SireMM.LJParameter()) + .molecule() + ) + mol_edit = ( + mol_edit.atom(atom.index()) + .setProperty("element1", _SireMol.Element(0)) + .molecule() + ) + if has_ambertype: + mol_edit = ( + mol_edit.atom(atom.index()) + .setProperty("ambertype1", "du") + .molecule() + ) + + # Update the Sire molecule object of the new molecule. + mol = mol_edit.commit() + # First work out the indices of atoms that are perturbed. pert_idxs = [] diff --git a/tests/Sandpit/Exscientia/Align/test_decouple.py b/tests/Sandpit/Exscientia/Align/test_decouple.py index 150938ddd..480ff8d44 100644 --- a/tests/Sandpit/Exscientia/Align/test_decouple.py +++ b/tests/Sandpit/Exscientia/Align/test_decouple.py @@ -81,8 +81,11 @@ def test_topology(mol, tmp_path): def test_end_types(mol): - """Check that the correct properties have been set at either - end of the perturbation.""" + """ + Check that the correct properties have been set for the 0 + end of the perturbation. Note that for SOMD, the 1 end state + properties are set in Process.Somd._to_pert_file. + """ decoupled_mol = decouple(mol) assert decoupled_mol._sire_object.property("charge0") == mol._sire_object.property( @@ -95,8 +98,3 @@ def test_end_types(mol): assert decoupled_mol._sire_object.property( "ambertype0" ) == mol._sire_object.property("ambertype") - for atom in decoupled_mol._sire_object.atoms(): - assert atom.property("charge1") == 0 * _SireUnits.e_charge - assert atom.property("LJ1") == _SireMM.LJParameter() - assert atom.property("element1") == _SireMol.Element(0) - assert atom.property("ambertype1") == "du" diff --git a/tests/Sandpit/Exscientia/FreeEnergy/test_alchemical_free_energy.py b/tests/Sandpit/Exscientia/FreeEnergy/test_alchemical_free_energy.py index 873c654d5..5aa89d5b4 100644 --- a/tests/Sandpit/Exscientia/FreeEnergy/test_alchemical_free_energy.py +++ b/tests/Sandpit/Exscientia/FreeEnergy/test_alchemical_free_energy.py @@ -1,4 +1,5 @@ import bz2 +from math import exp import pandas as pd import pathlib import pytest @@ -270,9 +271,11 @@ def freenrg(): return freenrg def test_files_exist(self, freenrg): - """Test if the files have been created. Note that e.g. gradients.dat + """ + Test if the files have been created. Note that e.g. gradients.dat are not created until later in the simulation, so their presence is - not tested for.""" + not tested for. + """ path = pathlib.Path(freenrg.workDir()) for lam in ["0.0000", "0.5000", "1.0000"]: assert (path / f"lambda_{lam}" / "simfile.dat").is_file() @@ -282,6 +285,31 @@ def test_files_exist(self, freenrg): assert (path / f"lambda_{lam}" / "somd.prm7").is_file() assert (path / f"lambda_{lam}" / "somd.err").is_file() assert (path / f"lambda_{lam}" / "somd.out").is_file() + assert (path / f"lambda_{lam}" / "somd.pert").is_file() + + def test_correct_pert_file(self, freenrg): + """Check that pert file is correct.""" + path = pathlib.Path(freenrg.workDir()) / "lambda_0.0000" + with open(os.path.join(path, "somd.pert"), "rt") as f: + lines = f.readlines() + + for i, line in enumerate(lines): + # Check that the end-state properties are correct. + if "final_type" in line: + assert "final_type du" in line + if "final_LJ" in line: + assert "final_LJ 0.00000 0.00000" in line + if "final_charge" in line: + assert "final_charge 0.00000" in line + # Check that the initial state properties are correct for the first and last atoms. + if line == " name C1\n": + assert "initial_type c" in lines[i + 1] + assert "initial_LJ 3.39967 0.08600" in lines[i + 3] + assert "initial_charge 0.67120" in lines[i + 5] + if "name O3" in line: + assert "initial_type o" in lines[i + 1] + assert "initial_LJ 2.95992 0.21000" in lines[i + 3] + assert "initial_charge -0.52110" in lines[i + 5] def test_correct_conf_file(self, freenrg): """Check that lambda data is correct in somd.cfg""" From 8c4e338810c359d9801c91fdc21e459aa371655e Mon Sep 17 00:00:00 2001 From: finlayclark Date: Tue, 27 Feb 2024 10:06:32 +0000 Subject: [PATCH 025/121] Remove extra line to satisfy black --- .../Sandpit/Exscientia/Process/_somd.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py index dc0ac4028..4466dd0fc 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_somd.py @@ -26,10 +26,10 @@ __all__ = ["Somd"] -from .._Utils import _try_import - import os as _os +from .._Utils import _try_import + _pygtail = _try_import("pygtail") import glob as _glob import random as _random @@ -38,24 +38,21 @@ import timeit as _timeit import warnings as _warnings -from sire.legacy import Base as _SireBase from sire.legacy import CAS as _SireCAS from sire.legacy import IO as _SireIO from sire.legacy import MM as _SireMM +from sire.legacy import Base as _SireBase from sire.legacy import Mol as _SireMol from sire.legacy import Units as _SireUnits -from .. import _isVerbose +from .. import IO as _IO +from .. import Protocol as _Protocol +from .. import Trajectory as _Trajectory +from .. import _isVerbose, _Utils from .._Exceptions import IncompatibleError as _IncompatibleError from .._Exceptions import MissingSoftwareError as _MissingSoftwareError from .._SireWrappers import Molecule as _Molecule from .._SireWrappers import System as _System - -from .. import IO as _IO -from .. import Protocol as _Protocol -from .. import Trajectory as _Trajectory -from .. import _Utils - from . import _process @@ -1034,7 +1031,6 @@ def _to_pert_file( # If the molecule is decoupled (for an ABFE calculation), then we need to # set the end-state properties of the molecule. if molecule.isDecoupled(): - # Invert the user property mappings. inv_property_map = {v: k for k, v in property_map.items()} From 3c6b55f2598abfe331c6997dd931128d3d90300d Mon Sep 17 00:00:00 2001 From: Zhiyi Wu Date: Mon, 4 Mar 2024 09:14:37 +0000 Subject: [PATCH 026/121] Only write couple molecule when the decoupled mol is not perturbable (#36) --- .../Sandpit/Exscientia/Protocol/_config.py | 47 ++++++++++--------- .../Exscientia/Protocol/test_config.py | 44 +++++++++++++++++ 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py index 4496107d7..6aca895fc 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py @@ -565,34 +565,35 @@ def generateGromacsConfig( [ mol, ] = self.system.getDecoupledMolecules() - decouple_dict = mol._sire_object.property("decouple") - protocol_dict["couple-moltype"] = mol._sire_object.name().value() - - def tranform(charge, LJ): - if charge and LJ: - return "vdw-q" - elif charge and not LJ: - return "q" - elif not charge and LJ: - return "vdw" - else: - return "none" + if not mol.isPerturbable(): + decouple_dict = mol._sire_object.property("decouple") + protocol_dict["couple-moltype"] = mol._sire_object.name().value() + + def tranform(charge, LJ): + if charge and LJ: + return "vdw-q" + elif charge and not LJ: + return "q" + elif not charge and LJ: + return "vdw" + else: + return "none" - protocol_dict["couple-lambda0"] = tranform( - decouple_dict["charge"][0], decouple_dict["LJ"][0] - ) - protocol_dict["couple-lambda1"] = tranform( - decouple_dict["charge"][1], decouple_dict["LJ"][1] - ) + protocol_dict["couple-lambda0"] = tranform( + decouple_dict["charge"][0], decouple_dict["LJ"][0] + ) + protocol_dict["couple-lambda1"] = tranform( + decouple_dict["charge"][1], decouple_dict["LJ"][1] + ) + if decouple_dict["intramol"].value(): + # The intramol is being coupled to the lambda change and thus being annihilated. + protocol_dict["couple-intramol"] = "yes" + else: + protocol_dict["couple-intramol"] = "no" # Add the soft-core parameters for the ABFE protocol_dict["sc-alpha"] = 0.5 protocol_dict["sc-power"] = 1 protocol_dict["sc-sigma"] = 0.3 - if decouple_dict["intramol"].value(): - # The intramol is being coupled to the lambda change and thus being annihilated. - protocol_dict["couple-intramol"] = "yes" - else: - protocol_dict["couple-intramol"] = "no" elif nDecoupledMolecules > 1: raise ValueError( "Gromacs cannot handle more than one decoupled molecule." diff --git a/tests/Sandpit/Exscientia/Protocol/test_config.py b/tests/Sandpit/Exscientia/Protocol/test_config.py index efb52898a..49e2d7b8a 100644 --- a/tests/Sandpit/Exscientia/Protocol/test_config.py +++ b/tests/Sandpit/Exscientia/Protocol/test_config.py @@ -18,6 +18,7 @@ from BioSimSpace.Sandpit.Exscientia.Units.Energy import kcal_per_mol from BioSimSpace.Sandpit.Exscientia.Units.Temperature import kelvin from BioSimSpace.Sandpit.Exscientia.FreeEnergy import Restraint +from BioSimSpace.Sandpit.Exscientia._SireWrappers import Molecule from BioSimSpace.Sandpit.Exscientia._Utils import _try_import, _have_imported @@ -301,6 +302,49 @@ def test_decouple_vdw_q(self, system): assert "couple-lambda1 = none" in mdp_text assert "couple-intramol = yes" in mdp_text + + def test_decouple_perturbable(self, system): + m, protocol = system + mol = decouple(m) + sire_mol = mol._sire_object + c = sire_mol.cursor() + for key in [ + "charge", + "LJ", + "bond", + "angle", + "dihedral", + "improper", + "forcefield", + "intrascale", + "mass", + "element", + "atomtype", + "coordinates", + "velocity", + "ambertype", + ]: + if f"{key}1" not in c and key in c: + c[f"{key}0"] = c[key] + c[f"{key}1"] = c[key] + + c["is_perturbable"] = True + sire_mol = c.commit() + mol = Molecule(sire_mol) + + freenrg = BSS.FreeEnergy.AlchemicalFreeEnergy( + mol.toSystem(), + protocol, + engine="GROMACS", + ) + with open(f"{freenrg._work_dir}/lambda_6/gromacs.mdp", "r") as f: + mdp_text = f.read() + assert "couple-moltype" not in mdp_text + assert "couple-lambda0" not in mdp_text + assert "couple-lambda1" not in mdp_text + assert "couple-intramol" not in mdp_text + + @pytest.mark.skipif( has_gromacs is False, reason="Requires GROMACS to be installed." ) From cb3748522a38715cfba7e4ebd275052d47dded5c Mon Sep 17 00:00:00 2001 From: Zhiyi Wu Date: Tue, 5 Mar 2024 15:07:28 +0000 Subject: [PATCH 027/121] Langevin integrator for Free Energy calculations (#37) --- .../Sandpit/Exscientia/Protocol/_config.py | 7 +++++-- tests/Sandpit/Exscientia/Protocol/test_config.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py index 6aca895fc..a0319ea2b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py @@ -516,8 +516,11 @@ def generateGromacsConfig( # Temperature control. if not isinstance(self.protocol, _Protocol.Minimisation): - protocol_dict["integrator"] = "md" # leap-frog dynamics. - protocol_dict["tcoupl"] = "v-rescale" + if isinstance(self.protocol, _Protocol._FreeEnergyMixin): + protocol_dict["integrator"] = "sd" # langevin dynamics. + else: + protocol_dict["integrator"] = "md" # leap-frog dynamics. + protocol_dict["tcoupl"] = "v-rescale" protocol_dict[ "tc-grps" ] = "system" # A single temperature group for the entire system. diff --git a/tests/Sandpit/Exscientia/Protocol/test_config.py b/tests/Sandpit/Exscientia/Protocol/test_config.py index 49e2d7b8a..b35ba1288 100644 --- a/tests/Sandpit/Exscientia/Protocol/test_config.py +++ b/tests/Sandpit/Exscientia/Protocol/test_config.py @@ -111,6 +111,18 @@ def test_tau_t(self, system, protocol): expected_res = {"tau-t = 2.00000"} assert expected_res.issubset(res) + @pytest.mark.parametrize( + "protocol", [Production, FreeEnergy] + ) + def test_integrator(self, system, protocol): + config = ConfigFactory(system, protocol(tau_t=BSS.Types.Time(2, "picosecond"))) + res = config.generateGromacsConfig() + if isinstance(protocol(), BSS.Protocol._FreeEnergyMixin): + expected_res = {'integrator = sd'} + else: + expected_res = {'integrator = md', 'tcoupl = v-rescale'} + assert expected_res.issubset(res) + @pytest.mark.skipif( has_gromacs is False, reason="Requires GROMACS to be installed." ) @@ -410,6 +422,7 @@ def test_sc_parameters(self, system): assert "sc-alpha = 0.5" in mdp_text + @pytest.mark.skipif( has_antechamber is False or has_openff is False, reason="Requires ambertools/antechamber and openff to be installed", From 234ca883ff86ef9fee4735e633ce526b668b5d31 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 8 Mar 2024 10:55:49 +0000 Subject: [PATCH 028/121] Only convert to end state if this isn't a FreeEnergy protocol. [closes #256] --- python/BioSimSpace/Process/_gromacs.py | 6 ++++-- python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index e4531dad3..166910e05 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -1995,8 +1995,10 @@ def _add_position_restraints(self): # Create a copy of the system. system = self._system.copy() - # Convert to the lambda = 0 state if this is a perturbable system. - system = self._checkPerturbable(system) + # Convert to the lambda = 0 state if this is a perturbable system and this + # isn't a free energy protocol. + if not isinstance(self._protocol, _FreeEnergyMixin): + system = self._checkPerturbable(system) # Convert the water model topology so that it matches the GROMACS naming convention. system._set_water_topology("GROMACS") diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py index 6d0bf4278..6c17685f4 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py @@ -2099,8 +2099,10 @@ def _add_position_restraints(self, config_options): # Create a copy of the system. system = self._system.copy() - # Convert to the lambda = 0 state if this is a perturbable system. - system = self._checkPerturbable(system) + # Convert to the lambda = 0 state if this is a perturbable system and this + # isn't a free energy protocol. + if not isinstance(self._protocol, _Protocol._FreeEnergyMixin): + system = self._checkPerturbable(system) # Convert the water model topology so that it matches the GROMACS naming convention. system._set_water_topology("GROMACS") From 5adf45587cf2cc2ee525a384ec1c1a5788b87678 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 8 Mar 2024 20:39:44 +0000 Subject: [PATCH 029/121] Exclude standard free ions from AMBER restraint mask. [closes #258] --- python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py | 4 ++-- python/BioSimSpace/_Config/_amber.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py index 4496107d7..a08935d9b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py @@ -293,9 +293,9 @@ def generateAmberConfig(self, extra_options=None, extra_lines=None): ] restraint_mask = "@" + ",".join(restraint_atom_names) elif restraint == "heavy": - restraint_mask = "!:WAT & !@H=" + restraint_mask = "!:WAT & !@%NA,CL & !@H=" elif restraint == "all": - restraint_mask = "!:WAT" + restraint_mask = "!:WAT & !@%NA,CL" # We can't do anything about a custom restraint, since we don't # know anything about the atoms. diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index a25a5109c..2a93e52a8 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -224,9 +224,9 @@ def createConfig( ] restraint_mask = "@" + ",".join(restraint_atom_names) elif restraint == "heavy": - restraint_mask = "!:WAT & !@H=" + restraint_mask = "!:WAT & !@%NA,CL & !@H=" elif restraint == "all": - restraint_mask = "!:WAT" + restraint_mask = "!:WAT & !@%NA,CL" # We can't do anything about a custom restraint, since we don't # know anything about the atoms. From 3a4a850d7ceef13a036f1d630f43a3345a4ec39a Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 8 Mar 2024 20:43:37 +0000 Subject: [PATCH 030/121] Add support for fractional exponents in __pow__ operator. [closes #259] --- .../Protocol/_position_restraint_mixin.py | 2 +- .../Protocol/_position_restraint.py | 2 +- .../Sandpit/Exscientia/Types/_angle.py | 5 +- .../Sandpit/Exscientia/Types/_area.py | 5 +- .../Sandpit/Exscientia/Types/_charge.py | 5 +- .../Sandpit/Exscientia/Types/_energy.py | 5 +- .../Sandpit/Exscientia/Types/_general_unit.py | 164 ++++++++++-------- .../Sandpit/Exscientia/Types/_length.py | 28 +-- .../Sandpit/Exscientia/Types/_pressure.py | 5 +- .../Sandpit/Exscientia/Types/_temperature.py | 22 +-- .../Sandpit/Exscientia/Types/_time.py | 5 +- .../Sandpit/Exscientia/Types/_type.py | 84 ++++----- .../Sandpit/Exscientia/Types/_volume.py | 5 +- python/BioSimSpace/Types/_angle.py | 5 +- python/BioSimSpace/Types/_area.py | 5 +- python/BioSimSpace/Types/_charge.py | 5 +- python/BioSimSpace/Types/_energy.py | 5 +- python/BioSimSpace/Types/_general_unit.py | 164 ++++++++++-------- python/BioSimSpace/Types/_length.py | 28 +-- python/BioSimSpace/Types/_pressure.py | 5 +- python/BioSimSpace/Types/_temperature.py | 15 +- python/BioSimSpace/Types/_time.py | 5 +- python/BioSimSpace/Types/_type.py | 84 ++++----- python/BioSimSpace/Types/_volume.py | 5 +- .../Exscientia/Types/test_general_unit.py | 76 +++++++- tests/Types/test_general_unit.py | 76 +++++++- 26 files changed, 441 insertions(+), 374 deletions(-) diff --git a/python/BioSimSpace/Protocol/_position_restraint_mixin.py b/python/BioSimSpace/Protocol/_position_restraint_mixin.py index b1fda37d8..8374bf8ad 100644 --- a/python/BioSimSpace/Protocol/_position_restraint_mixin.py +++ b/python/BioSimSpace/Protocol/_position_restraint_mixin.py @@ -214,7 +214,7 @@ def setForceConstant(self, force_constant): ) # Validate the dimensions. - if force_constant.dimensions() != (0, 0, 0, 1, -1, 0, -2): + if force_constant.dimensions() != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "'force_constant' has invalid dimensions! " f"Expected dimensions are 'M Q-1 T-2', found '{force_constant.unit()}'" diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py index ae3578870..38a82266d 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py @@ -185,7 +185,7 @@ def setForceConstant(self, force_constant): ) # Validate the dimensions. - if force_constant.dimensions() != (0, 0, 0, 1, -1, 0, -2): + if force_constant.dimensions() != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "'force_constant' has invalid dimensions! " f"Expected dimensions are 'M Q-1 T-2', found '{force_constant.unit()}'" diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py index f1fc753fa..b9bf75e6f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py @@ -52,9 +52,8 @@ class Angle(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "RADIAN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (1, 0, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py index 9e1eb9464..95a0d77e8 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py @@ -72,9 +72,8 @@ class Area(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM2" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py index 0c9879ed1..7093b84b9 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py @@ -58,9 +58,8 @@ class Charge(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ELECTRON CHARGE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 1, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py index fa775fb26..bbdea931e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py @@ -68,9 +68,8 @@ class Energy(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KILO CALORIES PER MOL" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 1, -1, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py index 256b9bd8b..b9c9d6b5b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py @@ -38,16 +38,16 @@ class GeneralUnit(_Type): """A general unit type.""" _dimension_chars = [ - "A", # Angle - "C", # Charge - "L", # Length "M", # Mass - "Q", # Quantity + "L", # Length + "T", # Time + "C", # Charge "t", # Temperature - "T", # Tme + "Q", # Quantity + "A", # Angle ] - def __new__(cls, *args): + def __new__(cls, *args, no_cast=False): """ Constructor. @@ -65,6 +65,9 @@ def __new__(cls, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ # This operator may be called when unpickling an object. Catch empty @@ -96,7 +99,7 @@ def __new__(cls, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -128,15 +131,7 @@ def __new__(cls, *args): general_unit = value * general_unit # Store the dimension mask. - dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + dimensions = tuple(general_unit.dimensions()) # This is a dimensionless quantity, return the value as a float. if all(x == 0 for x in dimensions): @@ -144,13 +139,13 @@ def __new__(cls, *args): # Check to see if the dimensions correspond to a supported type. # If so, return an object of that type. - if dimensions in _base_dimensions: + if not no_cast and dimensions in _base_dimensions: return _base_dimensions[dimensions](general_unit) # Otherwise, call __init__() else: return super(GeneralUnit, cls).__new__(cls) - def __init__(self, *args): + def __init__(self, *args, no_cast=False): """ Constructor. @@ -168,6 +163,9 @@ def __init__(self, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ value = 1 @@ -194,7 +192,7 @@ def __init__(self, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -222,15 +220,7 @@ def __init__(self, *args): self._value = self._sire_unit.value() # Store the dimension mask. - self._dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + self._dimensions = tuple(general_unit.dimensions()) # Create the unit string. self._unit = "" @@ -312,16 +302,8 @@ def __mul__(self, other): # Multipy the Sire unit objects. temp = self._sire_unit * other._to_sire_unit() - # Create the dimension mask. - dimensions = ( - temp.ANGLE(), - temp.CHARGE(), - temp.LENGTH(), - temp.MASS(), - temp.QUANTITY(), - temp.TEMPERATURE(), - temp.TIME(), - ) + # Get the dimension mask. + dimensions = temp.dimensions() # Return as an existing type if the dimensions match. try: @@ -432,24 +414,58 @@ def __rtruediv__(self, other): def __pow__(self, other): """Power operator.""" - if type(other) is not int: + if not isinstance(other, (int, float)): raise TypeError( "unsupported operand type(s) for ^: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + is_inverse = False + offset = 0 + + if isinstance(other, float): + if other > 1: + if not other.is_integer(): + raise ValueError("float exponent must be integer valued.") + else: + is_inverse = True + other = 1 / other + if not other.is_integer(): + raise ValueError( + "Divisor in fractional exponent must be integer valued." + ) + other = int(other) + if other == 0: return GeneralUnit(self._sire_unit / self._sire_unit) - # Multiply the Sire GeneralUnit 'other' times. - temp = self._sire_unit - for x in range(0, abs(other) - 1): - temp = temp * self._sire_unit + # Get the current unit dimemsions. + dims = self._sire_unit.dimensions() - if other > 0: - return GeneralUnit(temp) + # Check that the exponent is a factor of all the unit dimensions. + if is_inverse: + for dim in dims: + if dim % other != 0: + raise ValueError( + "The divisor of the exponent must be a factor of all the unit dimensions." + ) + + if is_inverse: + new_dims = [int(dim / other) for dim in dims] else: - return GeneralUnit(1 / temp) + new_dims = [int(dim * other) for dim in dims] + + if is_inverse: + value = self.value() ** (1 / other) + else: + value = self.value() ** other + + # Invert the value. + if other < 0 and not is_inverse: + value = 1 / value + + # Return a new GeneralUnit object. + return GeneralUnit(_GeneralUnit(value, new_dims)) def __lt__(self, other): """Less than operator.""" @@ -606,87 +622,87 @@ def dimensions(self): """ return self._dimensions - def angle(self): + def mass(self): """ - Return the power of this general unit in the 'angle' dimension. + Return the power of this general unit in the 'mass' dimension. Returns ------- - angle : int - The power of the general unit in the 'angle' dimension. + mass : int + The power of the general unit in the 'mass' dimension. """ return self._dimensions[0] - def charge(self): + def length(self): """ - Return the power of this general unit in the 'charge' dimension. + Return the power of this general unit in the 'length' dimension. Returns ------- - charge : int - The power of the general unit in the 'charge' dimension. + length : int + The power of the general unit in the 'length' dimension. """ return self._dimensions[1] - def length(self): + def time(self): """ - Return the power of this general unit in the 'length' dimension. + Return the power of this general unit in the 'time' dimension. Returns ------- - length : int - The power of the general unit in the 'length' dimension. + time : int + The power of the general unit in the 'time' dimension. """ return self._dimensions[2] - def mass(self): + def charge(self): """ - Return the power of this general unit in the 'mass' dimension. + Return the power of this general unit in the 'charge' dimension. Returns ------- - mass : int - The power of the general unit in the 'mass' dimension. + charge : int + The power of the general unit in the 'charge' dimension. """ return self._dimensions[3] - def quantity(self): + def temperature(self): """ - Return the power of this general unit in the 'quantity' dimension. + Return the power of this general unit in the 'temperature' dimension. Returns ------- - quantity : int - The power of the general unit in the 'quantity' dimension. + temperature : int + The power of the general unit in the 'temperature' dimension. """ return self._dimensions[4] - def temperature(self): + def quantity(self): """ - Return the power of this general unit in the 'temperature' dimension. + Return the power of this general unit in the 'quantity' dimension. Returns ------- - temperature : int - The power of the general unit in the 'temperature' dimension. + quantity : int + The power of the general unit in the 'quantity' dimension. """ return self._dimensions[5] - def time(self): + def angle(self): """ - Return the power of this general unit in the 'time' dimension. + Return the power of this general unit in the 'angle' dimension. Returns ------- - time : int - The power of the general unit in the 'time' dimension. + angle : int + The power of the general unit in the 'angle' dimension. """ return self._dimensions[6] diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py index 2096b3d20..907aa9ce7 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py @@ -87,9 +87,8 @@ class Length(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 1, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -195,29 +194,6 @@ def __rmul__(self, other): # Multiplication is commutative: a*b = b*a return self.__mul__(other) - def __pow__(self, other): - """Power operator.""" - - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - - # No change. - if other == 1: - return self - - # Area. - if other == 2: - mag = self.angstroms().value() ** 2 - return _Area(mag, "A2") - - # Volume. - if other == 3: - mag = self.angstroms().value() ** 3 - return _Volume(mag, "A3") - - else: - return super().__pow__(other) - def meters(self): """ Return the length in meters. diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py index 672dc2642..54c958f29 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py @@ -55,9 +55,8 @@ class Pressure(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ATMOSPHERE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, -1, 1, 0, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py index 8f0a7cb70..65ab604cc 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py @@ -60,9 +60,8 @@ class Temperature(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KELVIN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 1, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -409,9 +408,12 @@ def _validate_unit(self, unit): return unit elif unit in self._abbreviations: return self._abbreviations[unit] + elif len(unit) == 0: + raise ValueError(f"Unit is not given. You must supply the unit.") else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Unsupported unit '%s'. Supported units are: '%s'" + % (unit, list(self._supported_units.keys())) ) def _to_sire_unit(self): @@ -441,13 +443,13 @@ def _from_sire_unit(cls, sire_unit): if isinstance(sire_unit, _SireUnits.GeneralUnit): # Create a mask for the dimensions of the object. dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), + sire_unit.LENGTH(), sire_unit.TIME(), + sire_unit.CHARGE(), + sire_unit.TEMPERATURE(), + sire_unit.QUANTITY(), + sire_unit.ANGLE(), ) # Make sure the dimensions match. @@ -470,7 +472,7 @@ def _from_sire_unit(cls, sire_unit): else: raise TypeError( "'sire_unit' must be of type 'sire.units.GeneralUnit', " - "'Sire.Units.Celsius', or 'sire.units.Fahrenheit'" + "'sire.units.Celsius', or 'sire.units.Fahrenheit'" ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py index 0d1603205..eb715ea5f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py @@ -96,9 +96,8 @@ class Time(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "NANOSECOND" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 0, 1) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py index a0de475e3..168d80559 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py @@ -103,7 +103,7 @@ def __init__(self, *args): self._value = temp._value self._unit = temp._unit - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(args[0], str): # Convert the string to an object of this type. obj = self._from_string(args[0]) @@ -244,19 +244,9 @@ def __rmul__(self, other): def __pow__(self, other): """Power operator.""" - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - from ._general_unit import GeneralUnit as _GeneralUnit - default_unit = self._to_default_unit() - mag = default_unit.value() ** other - unit = default_unit.unit().lower() - pow_to_mul = "*".join(abs(other) * [unit]) - if other > 0: - return _GeneralUnit(f"{mag}*{pow_to_mul}") - else: - return _GeneralUnit(f"{mag}/({pow_to_mul})") + return _GeneralUnit(self._to_sire_unit(), no_cast=True) ** other def __truediv__(self, other): """Division operator.""" @@ -486,99 +476,99 @@ def dimensions(cls): containing the power in each dimension. Returns : (int, int, int, int, int, int) - The power in each dimension: 'angle', 'charge', 'length', - 'mass', 'quantity', 'temperature', and 'time'. + The power in each dimension: 'mass', 'length', 'temperature', + 'charge', 'time', 'quantity', and 'angle'. """ return cls._dimensions @classmethod - def angle(cls): + def mass(cls): """ - Return the power in the 'angle' dimension. + Return the power in the 'mass' dimension. Returns ------- - angle : int - The power in the 'angle' dimension. + mass : int + The power in the 'mass' dimension. """ return cls._dimensions[0] @classmethod - def charge(cls): + def length(cls): """ - Return the power in the 'charge' dimension. + Return the power in the 'length' dimension. Returns ------- - charge : int - The power in the 'charge' dimension. + length : int + The power in the 'length' dimension. """ return cls._dimensions[1] @classmethod - def length(cls): + def time(cls): """ - Return the power in the 'length' dimension. + Return the power in the 'time' dimension. Returns ------- - length : int - The power in the 'length' dimension. + time : int + The power the 'time' dimension. """ return cls._dimensions[2] @classmethod - def mass(cls): + def charge(cls): """ - Return the power in the 'mass' dimension. + Return the power in the 'charge' dimension. Returns ------- - mass : int - The power in the 'mass' dimension. + charge : int + The power in the 'charge' dimension. """ return cls._dimensions[3] @classmethod - def quantity(cls): + def temperature(cls): """ - Return the power in the 'quantity' dimension. + Return the power in the 'temperature' dimension. Returns ------- - quantity : int - The power in the 'quantity' dimension. + temperature : int + The power in the 'temperature' dimension. """ return cls._dimensions[4] @classmethod - def temperature(cls): + def quantity(cls): """ - Return the power in the 'temperature' dimension. + Return the power in the 'quantity' dimension. Returns ------- - temperature : int - The power in the 'temperature' dimension. + quantity : int + The power in the 'quantity' dimension. """ return cls._dimensions[5] @classmethod - def time(cls): + def angle(cls): """ - Return the power in the 'time' dimension. + Return the power in the 'angle' dimension. Returns ------- - time : int - The power the 'time' dimension. + angle : int + The power in the 'angle' dimension. """ return cls._dimensions[6] @@ -662,15 +652,7 @@ def _from_sire_unit(cls, sire_unit): raise TypeError("'sire_unit' must be of type 'sire.units.GeneralUnit'") # Create a mask for the dimensions of the object. - dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), - sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), - sire_unit.TIME(), - ) + dimensions = tuple(sire_unit.dimensions()) # Make sure that this isn't zero. if hasattr(sire_unit, "is_zero"): diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py index e664557da..bc827b93d 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py @@ -72,9 +72,8 @@ class Volume(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM3" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 3, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Types/_angle.py b/python/BioSimSpace/Types/_angle.py index f1fc753fa..b9bf75e6f 100644 --- a/python/BioSimSpace/Types/_angle.py +++ b/python/BioSimSpace/Types/_angle.py @@ -52,9 +52,8 @@ class Angle(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "RADIAN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (1, 0, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Types/_area.py b/python/BioSimSpace/Types/_area.py index 9e1eb9464..95a0d77e8 100644 --- a/python/BioSimSpace/Types/_area.py +++ b/python/BioSimSpace/Types/_area.py @@ -72,9 +72,8 @@ class Area(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM2" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Types/_charge.py b/python/BioSimSpace/Types/_charge.py index 0c9879ed1..7093b84b9 100644 --- a/python/BioSimSpace/Types/_charge.py +++ b/python/BioSimSpace/Types/_charge.py @@ -58,9 +58,8 @@ class Charge(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ELECTRON CHARGE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 1, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Types/_energy.py b/python/BioSimSpace/Types/_energy.py index fa775fb26..bbdea931e 100644 --- a/python/BioSimSpace/Types/_energy.py +++ b/python/BioSimSpace/Types/_energy.py @@ -68,9 +68,8 @@ class Energy(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KILO CALORIES PER MOL" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 1, -1, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Types/_general_unit.py b/python/BioSimSpace/Types/_general_unit.py index 256b9bd8b..b9c9d6b5b 100644 --- a/python/BioSimSpace/Types/_general_unit.py +++ b/python/BioSimSpace/Types/_general_unit.py @@ -38,16 +38,16 @@ class GeneralUnit(_Type): """A general unit type.""" _dimension_chars = [ - "A", # Angle - "C", # Charge - "L", # Length "M", # Mass - "Q", # Quantity + "L", # Length + "T", # Time + "C", # Charge "t", # Temperature - "T", # Tme + "Q", # Quantity + "A", # Angle ] - def __new__(cls, *args): + def __new__(cls, *args, no_cast=False): """ Constructor. @@ -65,6 +65,9 @@ def __new__(cls, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ # This operator may be called when unpickling an object. Catch empty @@ -96,7 +99,7 @@ def __new__(cls, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -128,15 +131,7 @@ def __new__(cls, *args): general_unit = value * general_unit # Store the dimension mask. - dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + dimensions = tuple(general_unit.dimensions()) # This is a dimensionless quantity, return the value as a float. if all(x == 0 for x in dimensions): @@ -144,13 +139,13 @@ def __new__(cls, *args): # Check to see if the dimensions correspond to a supported type. # If so, return an object of that type. - if dimensions in _base_dimensions: + if not no_cast and dimensions in _base_dimensions: return _base_dimensions[dimensions](general_unit) # Otherwise, call __init__() else: return super(GeneralUnit, cls).__new__(cls) - def __init__(self, *args): + def __init__(self, *args, no_cast=False): """ Constructor. @@ -168,6 +163,9 @@ def __init__(self, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ value = 1 @@ -194,7 +192,7 @@ def __init__(self, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -222,15 +220,7 @@ def __init__(self, *args): self._value = self._sire_unit.value() # Store the dimension mask. - self._dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + self._dimensions = tuple(general_unit.dimensions()) # Create the unit string. self._unit = "" @@ -312,16 +302,8 @@ def __mul__(self, other): # Multipy the Sire unit objects. temp = self._sire_unit * other._to_sire_unit() - # Create the dimension mask. - dimensions = ( - temp.ANGLE(), - temp.CHARGE(), - temp.LENGTH(), - temp.MASS(), - temp.QUANTITY(), - temp.TEMPERATURE(), - temp.TIME(), - ) + # Get the dimension mask. + dimensions = temp.dimensions() # Return as an existing type if the dimensions match. try: @@ -432,24 +414,58 @@ def __rtruediv__(self, other): def __pow__(self, other): """Power operator.""" - if type(other) is not int: + if not isinstance(other, (int, float)): raise TypeError( "unsupported operand type(s) for ^: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + is_inverse = False + offset = 0 + + if isinstance(other, float): + if other > 1: + if not other.is_integer(): + raise ValueError("float exponent must be integer valued.") + else: + is_inverse = True + other = 1 / other + if not other.is_integer(): + raise ValueError( + "Divisor in fractional exponent must be integer valued." + ) + other = int(other) + if other == 0: return GeneralUnit(self._sire_unit / self._sire_unit) - # Multiply the Sire GeneralUnit 'other' times. - temp = self._sire_unit - for x in range(0, abs(other) - 1): - temp = temp * self._sire_unit + # Get the current unit dimemsions. + dims = self._sire_unit.dimensions() - if other > 0: - return GeneralUnit(temp) + # Check that the exponent is a factor of all the unit dimensions. + if is_inverse: + for dim in dims: + if dim % other != 0: + raise ValueError( + "The divisor of the exponent must be a factor of all the unit dimensions." + ) + + if is_inverse: + new_dims = [int(dim / other) for dim in dims] else: - return GeneralUnit(1 / temp) + new_dims = [int(dim * other) for dim in dims] + + if is_inverse: + value = self.value() ** (1 / other) + else: + value = self.value() ** other + + # Invert the value. + if other < 0 and not is_inverse: + value = 1 / value + + # Return a new GeneralUnit object. + return GeneralUnit(_GeneralUnit(value, new_dims)) def __lt__(self, other): """Less than operator.""" @@ -606,87 +622,87 @@ def dimensions(self): """ return self._dimensions - def angle(self): + def mass(self): """ - Return the power of this general unit in the 'angle' dimension. + Return the power of this general unit in the 'mass' dimension. Returns ------- - angle : int - The power of the general unit in the 'angle' dimension. + mass : int + The power of the general unit in the 'mass' dimension. """ return self._dimensions[0] - def charge(self): + def length(self): """ - Return the power of this general unit in the 'charge' dimension. + Return the power of this general unit in the 'length' dimension. Returns ------- - charge : int - The power of the general unit in the 'charge' dimension. + length : int + The power of the general unit in the 'length' dimension. """ return self._dimensions[1] - def length(self): + def time(self): """ - Return the power of this general unit in the 'length' dimension. + Return the power of this general unit in the 'time' dimension. Returns ------- - length : int - The power of the general unit in the 'length' dimension. + time : int + The power of the general unit in the 'time' dimension. """ return self._dimensions[2] - def mass(self): + def charge(self): """ - Return the power of this general unit in the 'mass' dimension. + Return the power of this general unit in the 'charge' dimension. Returns ------- - mass : int - The power of the general unit in the 'mass' dimension. + charge : int + The power of the general unit in the 'charge' dimension. """ return self._dimensions[3] - def quantity(self): + def temperature(self): """ - Return the power of this general unit in the 'quantity' dimension. + Return the power of this general unit in the 'temperature' dimension. Returns ------- - quantity : int - The power of the general unit in the 'quantity' dimension. + temperature : int + The power of the general unit in the 'temperature' dimension. """ return self._dimensions[4] - def temperature(self): + def quantity(self): """ - Return the power of this general unit in the 'temperature' dimension. + Return the power of this general unit in the 'quantity' dimension. Returns ------- - temperature : int - The power of the general unit in the 'temperature' dimension. + quantity : int + The power of the general unit in the 'quantity' dimension. """ return self._dimensions[5] - def time(self): + def angle(self): """ - Return the power of this general unit in the 'time' dimension. + Return the power of this general unit in the 'angle' dimension. Returns ------- - time : int - The power of the general unit in the 'time' dimension. + angle : int + The power of the general unit in the 'angle' dimension. """ return self._dimensions[6] diff --git a/python/BioSimSpace/Types/_length.py b/python/BioSimSpace/Types/_length.py index 2096b3d20..907aa9ce7 100644 --- a/python/BioSimSpace/Types/_length.py +++ b/python/BioSimSpace/Types/_length.py @@ -87,9 +87,8 @@ class Length(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 1, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -195,29 +194,6 @@ def __rmul__(self, other): # Multiplication is commutative: a*b = b*a return self.__mul__(other) - def __pow__(self, other): - """Power operator.""" - - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - - # No change. - if other == 1: - return self - - # Area. - if other == 2: - mag = self.angstroms().value() ** 2 - return _Area(mag, "A2") - - # Volume. - if other == 3: - mag = self.angstroms().value() ** 3 - return _Volume(mag, "A3") - - else: - return super().__pow__(other) - def meters(self): """ Return the length in meters. diff --git a/python/BioSimSpace/Types/_pressure.py b/python/BioSimSpace/Types/_pressure.py index 672dc2642..54c958f29 100644 --- a/python/BioSimSpace/Types/_pressure.py +++ b/python/BioSimSpace/Types/_pressure.py @@ -55,9 +55,8 @@ class Pressure(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ATMOSPHERE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, -1, 1, 0, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Types/_temperature.py b/python/BioSimSpace/Types/_temperature.py index fe0693662..65ab604cc 100644 --- a/python/BioSimSpace/Types/_temperature.py +++ b/python/BioSimSpace/Types/_temperature.py @@ -60,9 +60,8 @@ class Temperature(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KELVIN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 1, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -444,13 +443,13 @@ def _from_sire_unit(cls, sire_unit): if isinstance(sire_unit, _SireUnits.GeneralUnit): # Create a mask for the dimensions of the object. dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), + sire_unit.LENGTH(), sire_unit.TIME(), + sire_unit.CHARGE(), + sire_unit.TEMPERATURE(), + sire_unit.QUANTITY(), + sire_unit.ANGLE(), ) # Make sure the dimensions match. diff --git a/python/BioSimSpace/Types/_time.py b/python/BioSimSpace/Types/_time.py index 0d1603205..eb715ea5f 100644 --- a/python/BioSimSpace/Types/_time.py +++ b/python/BioSimSpace/Types/_time.py @@ -96,9 +96,8 @@ class Time(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "NANOSECOND" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 0, 1) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/python/BioSimSpace/Types/_type.py b/python/BioSimSpace/Types/_type.py index a0de475e3..168d80559 100644 --- a/python/BioSimSpace/Types/_type.py +++ b/python/BioSimSpace/Types/_type.py @@ -103,7 +103,7 @@ def __init__(self, *args): self._value = temp._value self._unit = temp._unit - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(args[0], str): # Convert the string to an object of this type. obj = self._from_string(args[0]) @@ -244,19 +244,9 @@ def __rmul__(self, other): def __pow__(self, other): """Power operator.""" - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - from ._general_unit import GeneralUnit as _GeneralUnit - default_unit = self._to_default_unit() - mag = default_unit.value() ** other - unit = default_unit.unit().lower() - pow_to_mul = "*".join(abs(other) * [unit]) - if other > 0: - return _GeneralUnit(f"{mag}*{pow_to_mul}") - else: - return _GeneralUnit(f"{mag}/({pow_to_mul})") + return _GeneralUnit(self._to_sire_unit(), no_cast=True) ** other def __truediv__(self, other): """Division operator.""" @@ -486,99 +476,99 @@ def dimensions(cls): containing the power in each dimension. Returns : (int, int, int, int, int, int) - The power in each dimension: 'angle', 'charge', 'length', - 'mass', 'quantity', 'temperature', and 'time'. + The power in each dimension: 'mass', 'length', 'temperature', + 'charge', 'time', 'quantity', and 'angle'. """ return cls._dimensions @classmethod - def angle(cls): + def mass(cls): """ - Return the power in the 'angle' dimension. + Return the power in the 'mass' dimension. Returns ------- - angle : int - The power in the 'angle' dimension. + mass : int + The power in the 'mass' dimension. """ return cls._dimensions[0] @classmethod - def charge(cls): + def length(cls): """ - Return the power in the 'charge' dimension. + Return the power in the 'length' dimension. Returns ------- - charge : int - The power in the 'charge' dimension. + length : int + The power in the 'length' dimension. """ return cls._dimensions[1] @classmethod - def length(cls): + def time(cls): """ - Return the power in the 'length' dimension. + Return the power in the 'time' dimension. Returns ------- - length : int - The power in the 'length' dimension. + time : int + The power the 'time' dimension. """ return cls._dimensions[2] @classmethod - def mass(cls): + def charge(cls): """ - Return the power in the 'mass' dimension. + Return the power in the 'charge' dimension. Returns ------- - mass : int - The power in the 'mass' dimension. + charge : int + The power in the 'charge' dimension. """ return cls._dimensions[3] @classmethod - def quantity(cls): + def temperature(cls): """ - Return the power in the 'quantity' dimension. + Return the power in the 'temperature' dimension. Returns ------- - quantity : int - The power in the 'quantity' dimension. + temperature : int + The power in the 'temperature' dimension. """ return cls._dimensions[4] @classmethod - def temperature(cls): + def quantity(cls): """ - Return the power in the 'temperature' dimension. + Return the power in the 'quantity' dimension. Returns ------- - temperature : int - The power in the 'temperature' dimension. + quantity : int + The power in the 'quantity' dimension. """ return cls._dimensions[5] @classmethod - def time(cls): + def angle(cls): """ - Return the power in the 'time' dimension. + Return the power in the 'angle' dimension. Returns ------- - time : int - The power the 'time' dimension. + angle : int + The power in the 'angle' dimension. """ return cls._dimensions[6] @@ -662,15 +652,7 @@ def _from_sire_unit(cls, sire_unit): raise TypeError("'sire_unit' must be of type 'sire.units.GeneralUnit'") # Create a mask for the dimensions of the object. - dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), - sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), - sire_unit.TIME(), - ) + dimensions = tuple(sire_unit.dimensions()) # Make sure that this isn't zero. if hasattr(sire_unit, "is_zero"): diff --git a/python/BioSimSpace/Types/_volume.py b/python/BioSimSpace/Types/_volume.py index e664557da..bc827b93d 100644 --- a/python/BioSimSpace/Types/_volume.py +++ b/python/BioSimSpace/Types/_volume.py @@ -72,9 +72,8 @@ class Volume(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM3" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 3, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ diff --git a/tests/Sandpit/Exscientia/Types/test_general_unit.py b/tests/Sandpit/Exscientia/Types/test_general_unit.py index 3862c2a47..efc3cd4e1 100644 --- a/tests/Sandpit/Exscientia/Types/test_general_unit.py +++ b/tests/Sandpit/Exscientia/Types/test_general_unit.py @@ -3,15 +3,26 @@ import BioSimSpace.Sandpit.Exscientia.Types as Types import BioSimSpace.Sandpit.Exscientia.Units as Units +import sire as sr + @pytest.mark.parametrize( "string, dimensions", [ - ("kilo Cal oriEs per Mole / angstrom **2", (0, 0, 0, 1, -1, 0, -2)), - ("k Cal_per _mOl / nm^2", (0, 0, 0, 1, -1, 0, -2)), - ("kj p eR moles / pico METERs2", (0, 0, 0, 1, -1, 0, -2)), - ("coul oMbs / secs * ATm os phereS", (0, 1, -1, 1, 0, 0, -3)), - ("pm**3 * rads * de grEE", (2, 0, 3, 0, 0, 0, 0)), + ( + "kilo Cal oriEs per Mole / angstrom **2", + tuple(sr.u("kcal_per_mol / angstrom**2").dimensions()), + ), + ("k Cal_per _mOl / nm^2", tuple(sr.u("kcal_per_mol / nm**2").dimensions())), + ( + "kj p eR moles / pico METERs2", + tuple(sr.u("kJ_per_mol / pm**2").dimensions()), + ), + ( + "coul oMbs / secs * ATm os phereS", + tuple(sr.u("coulombs / second / atm").dimensions()), + ), + ("pm**3 * rads * de grEE", tuple(sr.u("pm**3 * rad * degree").dimensions())), ], ) def test_supported_units(string, dimensions): @@ -140,6 +151,61 @@ def test_neg_pow(unit_type): assert d1 == -d0 +def test_frac_pow(): + """Test that unit-based types can be raised to fractional powers.""" + + # Create a base unit type. + unit_type = 2 * Units.Length.angstrom + + # Store the original value and dimensions. + value = unit_type.value() + dimensions = unit_type.dimensions() + + # Square the type. + unit_type = unit_type**2 + + # Assert that we can't take the cube root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 3) + + # Now take the square root. + unit_type = unit_type ** (1 / 2) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Cube the type. + unit_type = unit_type**3 + + # Assert that we can't take the square root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 2) + + # Now take the cube root. + unit_type = unit_type ** (1 / 3) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Square the type again. + unit_type = unit_type**2 + + # Now take the negative square root. + unit_type = unit_type ** (-1 / 2) + + # The value should be inverted. + assert unit_type.value() == 1 / value + + # The dimensions should be negated. + assert unit_type.dimensions() == tuple(-d for d in dimensions) + + @pytest.mark.parametrize( "string", [ diff --git a/tests/Types/test_general_unit.py b/tests/Types/test_general_unit.py index d97acaf36..ec630d060 100644 --- a/tests/Types/test_general_unit.py +++ b/tests/Types/test_general_unit.py @@ -3,15 +3,26 @@ import BioSimSpace.Types as Types import BioSimSpace.Units as Units +import sire as sr + @pytest.mark.parametrize( "string, dimensions", [ - ("kilo Cal oriEs per Mole / angstrom **2", (0, 0, 0, 1, -1, 0, -2)), - ("k Cal_per _mOl / nm^2", (0, 0, 0, 1, -1, 0, -2)), - ("kj p eR moles / pico METERs2", (0, 0, 0, 1, -1, 0, -2)), - ("coul oMbs / secs * ATm os phereS", (0, 1, -1, 1, 0, 0, -3)), - ("pm**3 * rads * de grEE", (2, 0, 3, 0, 0, 0, 0)), + ( + "kilo Cal oriEs per Mole / angstrom **2", + tuple(sr.u("kcal_per_mol / angstrom**2").dimensions()), + ), + ("k Cal_per _mOl / nm^2", tuple(sr.u("kcal_per_mol / nm**2").dimensions())), + ( + "kj p eR moles / pico METERs2", + tuple(sr.u("kJ_per_mol / pm**2").dimensions()), + ), + ( + "coul oMbs / secs * ATm os phereS", + tuple(sr.u("coulombs / second / atm").dimensions()), + ), + ("pm**3 * rads * de grEE", tuple(sr.u("pm**3 * rad * degree").dimensions())), ], ) def test_supported_units(string, dimensions): @@ -140,6 +151,61 @@ def test_neg_pow(unit_type): assert d1 == -d0 +def test_frac_pow(): + """Test that unit-based types can be raised to fractional powers.""" + + # Create a base unit type. + unit_type = 2 * Units.Length.angstrom + + # Store the original value and dimensions. + value = unit_type.value() + dimensions = unit_type.dimensions() + + # Square the type. + unit_type = unit_type**2 + + # Assert that we can't take the cube root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 3) + + # Now take the square root. + unit_type = unit_type ** (1 / 2) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Cube the type. + unit_type = unit_type**3 + + # Assert that we can't take the square root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 2) + + # Now take the cube root. + unit_type = unit_type ** (1 / 3) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Square the type again. + unit_type = unit_type**2 + + # Now take the negative square root. + unit_type = unit_type ** (-1 / 2) + + # The value should be inverted. + assert unit_type.value() == 1 / value + + # The dimensions should be negated. + assert unit_type.dimensions() == tuple(-d for d in dimensions) + + @pytest.mark.parametrize( "string", [ From 2d96b172397b6e318797fbbcca9dd722c34224a2 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 11 Mar 2024 08:57:01 +0000 Subject: [PATCH 031/121] Fix dimension mask for restraint force constants. --- .../BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py index 0cd0b3d97..d2432cf5f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py @@ -174,7 +174,7 @@ def __init__(self, system, restraint_dict, temperature, restraint_type="Boresch" for key in ["kthetaA", "kthetaB", "kphiA", "kphiB", "kphiC"]: if restraint_dict["force_constants"][key] != 0: dim = restraint_dict["force_constants"][key].dimensions() - if dim != (-2, 0, 2, 1, -1, 0, -2): + if dim != (1, 2, -2, 0, 0, -1, -2): raise ValueError( f"restraint_dict['force_constants']['{key}'] must be of type " f"'BioSimSpace.Types.Energy'/'BioSimSpace.Types.Angle^2'" @@ -202,7 +202,7 @@ def __init__(self, system, restraint_dict, temperature, restraint_type="Boresch" # Test if the force constant of the bond r1-l1 is the correct unit # Such as kcal/mol/angstrom^2 dim = restraint_dict["force_constants"]["kr"].dimensions() - if dim != (0, 0, 0, 1, -1, 0, -2): + if dim != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "restraint_dict['force_constants']['kr'] must be of type " "'BioSimSpace.Types.Energy'/'BioSimSpace.Types.Length^2'" From 1ff16e9b4f0a8fe6963c3937fe6303f0509b188c Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 11 Mar 2024 09:45:59 +0000 Subject: [PATCH 032/121] Another dimension mask fix. --- .../BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py index d2432cf5f..ab961ddb2 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py @@ -290,13 +290,13 @@ def __init__(self, system, restraint_dict, temperature, restraint_type="Boresch" "'BioSimSpace.Types.Length'" ) if not single_restraint_dict["kr"].dimensions() == ( + 1, 0, + -2, 0, 0, - 1, -1, 0, - -2, ): raise ValueError( "distance_restraint_dict['kr'] must be of type " From 33d5a15235ff82340411a4c1bcb1f75c92b95a80 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 11 Mar 2024 10:00:51 +0000 Subject: [PATCH 033/121] Handle addition or subtraction by a zero-valued integer or float. --- .../Sandpit/Exscientia/Types/_general_unit.py | 22 ++++++++++++++++- .../Sandpit/Exscientia/Types/_type.py | 24 +++++++++++++++++-- python/BioSimSpace/Types/_general_unit.py | 22 ++++++++++++++++- python/BioSimSpace/Types/_type.py | 24 +++++++++++++++++-- 4 files changed, 86 insertions(+), 6 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py index b9c9d6b5b..971a39b30 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py @@ -261,12 +261,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -275,17 +285,27 @@ def __sub__(self, other): temp = self._sire_unit - other._to_sire_unit() return GeneralUnit(temp) - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py index 168d80559..75d7b7e0c 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py @@ -168,12 +168,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -185,22 +195,32 @@ def __sub__(self, other): # Return a new object of the same type with the original unit. return self._to_default_unit(val)._convert_to(self._unit) - # Addition of a different type with the same dimensions. + # Subtraction of a different type with the same dimensions. elif isinstance(other, Type) and self._dimensions == other.dimensions: # Negate other and add. return -other + self - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" diff --git a/python/BioSimSpace/Types/_general_unit.py b/python/BioSimSpace/Types/_general_unit.py index b9c9d6b5b..971a39b30 100644 --- a/python/BioSimSpace/Types/_general_unit.py +++ b/python/BioSimSpace/Types/_general_unit.py @@ -261,12 +261,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -275,17 +285,27 @@ def __sub__(self, other): temp = self._sire_unit - other._to_sire_unit() return GeneralUnit(temp) - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" diff --git a/python/BioSimSpace/Types/_type.py b/python/BioSimSpace/Types/_type.py index 168d80559..75d7b7e0c 100644 --- a/python/BioSimSpace/Types/_type.py +++ b/python/BioSimSpace/Types/_type.py @@ -168,12 +168,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -185,22 +195,32 @@ def __sub__(self, other): # Return a new object of the same type with the original unit. return self._to_default_unit(val)._convert_to(self._unit) - # Addition of a different type with the same dimensions. + # Subtraction of a different type with the same dimensions. elif isinstance(other, Type) and self._dimensions == other.dimensions: # Negate other and add. return -other + self - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" From 3709cda1cf7017ec054175569d5767a7e733b2ed Mon Sep 17 00:00:00 2001 From: Zhiyi Wu Date: Mon, 11 Mar 2024 10:03:24 +0000 Subject: [PATCH 034/121] Allow Gromacs to write dihedral_restraints in the presence of restraint-lambdas (#38) --- .../Exscientia/FreeEnergy/_restraint.py | 134 ++++++++++++++---- .../Sandpit/Exscientia/Process/_gromacs.py | 2 + .../Exscientia/FreeEnergy/test_restraint.py | 26 ++++ .../Exscientia/Process/test_gromacs.py | 128 ++++++++++------- 4 files changed, 210 insertions(+), 80 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py index 499846255..74d9652ee 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py @@ -380,7 +380,7 @@ def system(self, system): # Store a copy of solvated system. self._system = system.copy() - def _gromacs_boresch(self, perturbation_type=None): + def _gromacs_boresch(self, perturbation_type=None, restraint_lambda=False): """Format the Gromacs string for boresch restraint.""" # Format the atoms into index list @@ -398,9 +398,18 @@ def format_index(key_list): return " ".join(formated_index) parameters_string = "{eq0:<10} {fc0:<10} {eq1:<10} {fc1:<10}" + # The Gromacs dihedral restraints has the format of + # phi dphi fc and we don't want dphi for the restraint, it is hence zero + dihedral_restraints_parameters_string = ( + "{eq0:<10} 0.00 {fc0:<10} {eq1:<10} 0.00 {fc1:<10}" + ) # Format the parameters for the bonds def format_bond(equilibrium_values, force_constants): + """ + Format the bonds equilibrium values and force constant + in into the Gromacs topology format. + """ converted_equ_val = ( self._restraint_dict["equilibrium_values"][equilibrium_values] / _nanometer @@ -416,14 +425,33 @@ def format_bond(equilibrium_values, force_constants): ) # Format the parameters for the angles and dihedrals - def format_angle(equilibrium_values, force_constants): + def format_angle(equilibrium_values, force_constants, restraint_lambda): + """ + Format the angle equilibrium values and force constant + in into the Gromacs topology format. + + For Boresch restraint, we might want the dihedral to be stored + under the [ dihedral_restraints ] and controlled by restraint-lambdas. + Instead of under the [ dihedrals ] directive and controlled by bonded-lambdas. + + However, for dihedrals, the [ dihedral_restraints ] has a different function type + compared with [ dihedrals ] and more values for the force constant, so we need + to format them differently. + + When restraint_lambda is True, the dihedrals will be stored in the dihedral_restraints. + """ converted_equ_val = ( self._restraint_dict["equilibrium_values"][equilibrium_values] / _degree ) converted_fc = self._restraint_dict["force_constants"][force_constants] / ( _kj_per_mol / (_radian * _radian) ) - return parameters_string.format( + par_string = ( + dihedral_restraints_parameters_string + if restraint_lambda + else parameters_string + ) + return par_string.format( eq0="{:.3f}".format(converted_equ_val), fc0="{:.2f}".format(0), eq1="{:.3f}".format(converted_equ_val), @@ -444,14 +472,28 @@ def write_angle(key_list, equilibrium_values, force_constants): return master_string.format( index=format_index(key_list), func_type=1, - parameters=format_angle(equilibrium_values, force_constants), + parameters=format_angle( + equilibrium_values, force_constants, restraint_lambda=False + ), ) - def write_dihedral(key_list, equilibrium_values, force_constants): + def write_dihedral( + key_list, equilibrium_values, force_constants, restraint_lambda + ): + if restraint_lambda: + # In [ dihedral_restraints ], function type 1 + # means the dihedral is restrained harmonically. + func_type = 1 + else: + # In [ dihedrals ], function type 2 + # means the dihedral is restrained harmonically. + func_type = 2 return master_string.format( index=format_index(key_list), - func_type=2, - parameters=format_angle(equilibrium_values, force_constants), + func_type=func_type, + parameters=format_angle( + equilibrium_values, force_constants, restraint_lambda + ), ) # Writing the string @@ -473,16 +515,43 @@ def write_dihedral(key_list, equilibrium_values, force_constants): # Angles: r1-l1-l2 (thetaB0, kthetaB) output.append(write_angle(("r1", "l1", "l2"), "thetaB0", "kthetaB")) - output.append("[ dihedrals ]") + if restraint_lambda: + output.append("[ dihedral_restraints ]") + output.append( + "; ai aj ak al type phiA dphiA fcA phiB dphiB fcB" + ) + else: + output.append("[ dihedrals ]") + output.append( + "; ai aj ak al type phiA fcA phiB fcB" + ) + # Dihedrals: r3-r2-r1-l1 (phiA0, kphiA) output.append( - "; ai aj ak al type phiA fcA phiB fcB" + write_dihedral( + ("r3", "r2", "r1", "l1"), + "phiA0", + "kphiA", + restraint_lambda=restraint_lambda, + ) ) - # Dihedrals: r3-r2-r1-l1 (phiA0, kphiA) - output.append(write_dihedral(("r3", "r2", "r1", "l1"), "phiA0", "kphiA")) # Dihedrals: r2-r1-l1-l2 (phiB0, kphiB) - output.append(write_dihedral(("r2", "r1", "l1", "l2"), "phiB0", "kphiB")) + output.append( + write_dihedral( + ("r2", "r1", "l1", "l2"), + "phiB0", + "kphiB", + restraint_lambda=restraint_lambda, + ) + ) # Dihedrals: r1-l1-l2-l3 (phiC0, kphiC) - output.append(write_dihedral(("r1", "l1", "l2", "l3"), "phiC0", "kphiC")) + output.append( + write_dihedral( + ("r1", "l1", "l2", "l3"), + "phiC0", + "kphiC", + restraint_lambda=restraint_lambda, + ) + ) return "\n".join(output) @@ -702,7 +771,7 @@ def _add_restr_to_str(restr, restr_string): ) return standard_restr_string + permanent_restr_string[:-2] + "}" - def toString(self, engine, perturbation_type=None): + def toString(self, engine, perturbation_type=None, restraint_lambda=False): """ The method for convert the restraint to a format that could be used by MD Engines. @@ -721,25 +790,28 @@ def toString(self, engine, perturbation_type=None): turned on when the perturbation type is "restraint", but for which the permanent distance restraint is always active if the perturbation type is "release_restraint" (or any other perturbation type). + restraint_lambda : str, optional, default=False + Whether to use restraint_lambda in Gromacs, this would move the dihedral restraints + from [ dihedrals ], which is controlled by the bonded-lambda to + [ dihedral_restraints ], which is controlled by restraint-lambda. """ - to_str_functions = { - "boresch": {"gromacs": self._gromacs_boresch, "somd": self._somd_boresch}, - "multiple_distance": { - "gromacs": self._gromacs_multiple_distance, - "somd": self._somd_multiple_distance, - }, - } - engine = engine.strip().lower() - try: - str_fn = to_str_functions[self._restraint_type][engine] - except KeyError: - raise NotImplementedError( - f"Restraint type {self._restraint_type} not implemented " - f"yet for {engine}." - ) - - return str_fn(perturbation_type) + match (self._restraint_type, engine): + case "boresch", "gromacs": + return self._gromacs_boresch( + perturbation_type, restraint_lambda=restraint_lambda + ) + case "boresch", "somd": + return self._somd_boresch(perturbation_type) + case "multiple_distance", "gromacs": + return self._gromacs_multiple_distance(perturbation_type) + case "multiple_distance", "somd": + return self._somd_multiple_distance(perturbation_type) + case _: + raise NotImplementedError( + f"Restraint type {self._restraint_type} not implemented " + f"yet for {engine}." + ) def getCorrection(self, method="analytical"): """ diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py index 6d0bf4278..2586be104 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py @@ -407,6 +407,8 @@ def _write_system(self, system, coord_file=None, topol_file=None, ref_file=None) self._restraint.toString( engine="GROMACS", perturbation_type=self._protocol.getPerturbationType(), + restraint_lambda="restraint" + in self._protocol.getLambda(type="series"), ) ) diff --git a/tests/Sandpit/Exscientia/FreeEnergy/test_restraint.py b/tests/Sandpit/Exscientia/FreeEnergy/test_restraint.py index e5d7ce24d..6c88cb050 100644 --- a/tests/Sandpit/Exscientia/FreeEnergy/test_restraint.py +++ b/tests/Sandpit/Exscientia/FreeEnergy/test_restraint.py @@ -171,7 +171,9 @@ def test_dihedral(self, Topology): assert aj == "2" assert ak == "1" assert al == "1496" + assert type == "2" assert phiA == "148.396" + assert kA == "0.00" assert phiB == "148.396" assert kB == "41.84" ai, aj, ak, al, type, phiA, kA, phiB, kB = Topology[11].split() @@ -186,6 +188,30 @@ def test_dihedral(self, Topology): assert al == "1498" +class TestGromacsOutputBoreschRestraintLambda(TestGromacsOutputBoresch): + @staticmethod + @pytest.fixture(scope="class") + def Topology(boresch_restraint): + return boresch_restraint.toString( + engine="Gromacs", restraint_lambda=True + ).split("\n") + + def test_dihedral(self, Topology): + assert "dihedral_restraints" in Topology[8] + ai, aj, ak, al, type, phiA, dphiA, kA, phiB, dphiB, kB = Topology[10].split() + assert ai == "3" + assert aj == "2" + assert ak == "1" + assert al == "1496" + assert type == "1" + assert phiA == "148.396" + assert dphiA == "0.00" + assert dphiB == "0.00" + assert kA == "0.00" + assert phiB == "148.396" + assert kB == "41.84" + + class TestSomdOutputBoresch: @staticmethod @pytest.fixture(scope="class") diff --git a/tests/Sandpit/Exscientia/Process/test_gromacs.py b/tests/Sandpit/Exscientia/Process/test_gromacs.py index ec3b9b7f7..09c3e3564 100644 --- a/tests/Sandpit/Exscientia/Process/test_gromacs.py +++ b/tests/Sandpit/Exscientia/Process/test_gromacs.py @@ -167,57 +167,87 @@ def test_restraints(perturbable_system, restraint): has_gromacs is False or has_pyarrow is False, reason="Requires GROMACS and pyarrow to be installed.", ) -def test_write_restraint(system, tmp_path): - """Test if the restraint has been written in a way that could be processed - correctly. - """ - ligand = ligand = BSS.IO.readMolecules( - [f"{url}/ligand01.prm7.bz2", f"{url}/ligand01.rst7.bz2"] - ).getMolecule(0) - decoupled_ligand = decouple(ligand) - l1 = decoupled_ligand.getAtoms()[0] - l2 = decoupled_ligand.getAtoms()[1] - l3 = decoupled_ligand.getAtoms()[2] - ligand_2 = BSS.IO.readMolecules( - [f"{url}/ligand04.prm7.bz2", f"{url}/ligand04.rst7.bz2"] - ).getMolecule(0) - r1 = ligand_2.getAtoms()[0] - r2 = ligand_2.getAtoms()[1] - r3 = ligand_2.getAtoms()[2] - system = (decoupled_ligand + ligand_2).toSystem() - - restraint_dict = { - "anchor_points": {"r1": r1, "r2": r2, "r3": r3, "l1": l1, "l2": l2, "l3": l3}, - "equilibrium_values": { - "r0": 7.84 * angstrom, - "thetaA0": 0.81 * radian, - "thetaB0": 1.74 * radian, - "phiA0": 2.59 * radian, - "phiB0": -1.20 * radian, - "phiC0": 2.63 * radian, - }, - "force_constants": { - "kr": 10 * kcal_per_mol / angstrom**2, - "kthetaA": 10 * kcal_per_mol / (radian * radian), - "kthetaB": 10 * kcal_per_mol / (radian * radian), - "kphiA": 10 * kcal_per_mol / (radian * radian), - "kphiB": 10 * kcal_per_mol / (radian * radian), - "kphiC": 10 * kcal_per_mol / (radian * radian), - }, - } - restraint = Restraint( - system, restraint_dict, 300 * kelvin, restraint_type="Boresch" - ) +class TestRestraint: + @pytest.fixture(scope="class") + def setup(self): + ligand = BSS.IO.readMolecules( + [f"{url}/ligand01.prm7.bz2", f"{url}/ligand01.rst7.bz2"] + ).getMolecule(0) + decoupled_ligand = decouple(ligand) + l1 = decoupled_ligand.getAtoms()[0] + l2 = decoupled_ligand.getAtoms()[1] + l3 = decoupled_ligand.getAtoms()[2] + ligand_2 = BSS.IO.readMolecules( + [f"{url}/ligand04.prm7.bz2", f"{url}/ligand04.rst7.bz2"] + ).getMolecule(0) + r1 = ligand_2.getAtoms()[0] + r2 = ligand_2.getAtoms()[1] + r3 = ligand_2.getAtoms()[2] + system = (decoupled_ligand + ligand_2).toSystem() + restraint_dict = { + "anchor_points": { + "r1": r1, + "r2": r2, + "r3": r3, + "l1": l1, + "l2": l2, + "l3": l3, + }, + "equilibrium_values": { + "r0": 7.84 * angstrom, + "thetaA0": 0.81 * radian, + "thetaB0": 1.74 * radian, + "phiA0": 2.59 * radian, + "phiB0": -1.20 * radian, + "phiC0": 2.63 * radian, + }, + "force_constants": { + "kr": 10 * kcal_per_mol / angstrom**2, + "kthetaA": 10 * kcal_per_mol / (radian * radian), + "kthetaB": 10 * kcal_per_mol / (radian * radian), + "kphiA": 10 * kcal_per_mol / (radian * radian), + "kphiB": 10 * kcal_per_mol / (radian * radian), + "kphiC": 10 * kcal_per_mol / (radian * radian), + }, + } + restraint = Restraint( + system, restraint_dict, 300 * kelvin, restraint_type="Boresch" + ) + return system, restraint + + def test_regular_protocol(self, setup, tmp_path_factory): + """Test if the restraint has been written in a way that could be processed + correctly. + """ + tmp_path = tmp_path_factory.mktemp("out") + system, restraint = setup + # Create a short production protocol. + protocol = BSS.Protocol.FreeEnergy( + runtime=BSS.Types.Time(0.0001, "nanoseconds"), perturbation_type="full" + ) - # Create a short production protocol. - protocol = BSS.Protocol.FreeEnergy( - runtime=BSS.Types.Time(0.0001, "nanoseconds"), perturbation_type="full" - ) + # Run the process and check that it finishes without error. + run_process(system, protocol, restraint=restraint, work_dir=str(tmp_path)) + with open(tmp_path / "test.top", "r") as f: + assert "intermolecular_interactions" in f.read() + + def test_restraint_lambda(self, setup, tmp_path_factory): + """Test if the restraint has been written correctly when restraint lambda is evoked.""" + tmp_path = tmp_path_factory.mktemp("out") + system, restraint = setup + # Create a short production protocol. + protocol = BSS.Protocol.FreeEnergy( + runtime=BSS.Types.Time(0.0001, "nanoseconds"), + lam=pd.Series(data={"bonded": 0.0, "restraint": 0.0}), + lam_vals=pd.DataFrame(data={"bonded": [0.0, 1.0], "restraint": [0.0, 1.0]}), + ) - # Run the process and check that it finishes without error. - run_process(system, protocol, restraint=restraint, work_dir=str(tmp_path)) - with open(tmp_path / "test.top", "r") as f: - assert "intermolecular_interactions" in f.read() + # Run the process and check that it finishes without error. + run_process(system, protocol, restraint=restraint, work_dir=str(tmp_path)) + with open(tmp_path / "test.top", "r") as f: + assert "dihedral_restraints" in f.read() + with open(tmp_path / "test.mdp", "r") as f: + assert "restraint-lambdas" in f.read() def run_process(system, protocol, **kwargs): From 245aacba57e9992ce1cb55ee61bc0275b9cbf664 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 11 Mar 2024 11:16:47 +0000 Subject: [PATCH 035/121] Validate dimension mask in test, not unit string. --- .../Sandpit/Exscientia/FreeEnergy/_restraint_search.py | 2 +- .../Exscientia/FreeEnergy/test_restraint_search.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py index cb501bbb8..f7ec62dd4 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py @@ -641,7 +641,7 @@ def analyse( if force_constant: dim = force_constant.dimensions() - if dim != (0, 0, 0, 1, -1, 0, -2): + if dim != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "force_constant must be of type " "'BioSimSpace.Types.Energy'/'BioSimSpace.Types.Length^2'" diff --git a/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py b/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py index c7489f8d2..b34496bb0 100644 --- a/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py +++ b/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py @@ -291,7 +291,15 @@ def test_dict_mdr(self, multiple_distance_restraint): assert restr_dict["permanent_distance_restraint"][ "r0" ].value() == pytest.approx(8.9019, abs=1e-4) - assert restr_dict["permanent_distance_restraint"]["kr"].unit() == "M Q-1 T-2" + assert restr_dict["permanent_distance_restraint"]["kr"].dimensions() == ( + 1, + 0, + -2, + 0, + 0, + -1, + 0, + ) assert restr_dict["permanent_distance_restraint"]["kr"].value() == 40.0 assert restr_dict["permanent_distance_restraint"]["r_fb"].unit() == "ANGSTROM" assert restr_dict["permanent_distance_restraint"][ From 112614362b284f9b576c8c3b2128121adc417568 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 11 Mar 2024 12:27:41 +0000 Subject: [PATCH 036/121] Switch to classmethods. --- .../Sandpit/Exscientia/Types/_angle.py | 11 ++++++----- .../Sandpit/Exscientia/Types/_area.py | 11 ++++++----- .../Sandpit/Exscientia/Types/_charge.py | 9 +++++---- .../Sandpit/Exscientia/Types/_energy.py | 9 +++++---- .../Sandpit/Exscientia/Types/_length.py | 11 ++++++----- .../Sandpit/Exscientia/Types/_pressure.py | 9 +++++---- .../Sandpit/Exscientia/Types/_temperature.py | 11 ++++++----- .../Sandpit/Exscientia/Types/_time.py | 17 +++++++++-------- .../Sandpit/Exscientia/Types/_volume.py | 11 ++++++----- python/BioSimSpace/Types/_angle.py | 11 ++++++----- python/BioSimSpace/Types/_area.py | 11 ++++++----- python/BioSimSpace/Types/_charge.py | 9 +++++---- python/BioSimSpace/Types/_energy.py | 9 +++++---- python/BioSimSpace/Types/_length.py | 11 ++++++----- python/BioSimSpace/Types/_pressure.py | 9 +++++---- python/BioSimSpace/Types/_temperature.py | 11 ++++++----- python/BioSimSpace/Types/_time.py | 17 +++++++++-------- python/BioSimSpace/Types/_volume.py | 11 ++++++----- 18 files changed, 108 insertions(+), 90 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py index b9bf75e6f..cef19c862 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py @@ -187,7 +187,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -209,13 +210,13 @@ def _validate_unit(self, unit): unit = unit.replace("AD", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py index 95a0d77e8..2763618a9 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py @@ -329,7 +329,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit is supported.""" # Strip whitespace and convert to upper case. @@ -359,13 +360,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "2" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py index 7093b84b9..a65d54cd3 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py @@ -181,7 +181,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -212,11 +213,11 @@ def _validate_unit(self, unit): unit = unit.replace("COUL", "C") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py index bbdea931e..af9aa2894 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py @@ -212,7 +212,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -234,11 +235,11 @@ def _validate_unit(self, unit): unit = unit.replace("JOULES", "J") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py index 907aa9ce7..67f163af8 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py @@ -338,7 +338,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -352,13 +353,13 @@ def _validate_unit(self, unit): unit = "ANGS" + unit[3:] # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py index 54c958f29..fbe6da782 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py @@ -176,7 +176,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -195,11 +196,11 @@ def _validate_unit(self, unit): unit = unit.replace("S", "") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py index 65ab604cc..a7010dd7f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py @@ -391,7 +391,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -404,16 +405,16 @@ def _validate_unit(self, unit): unit = unit.replace("DEG", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] elif len(unit) == 0: raise ValueError(f"Unit is not given. You must supply the unit.") else: raise ValueError( "Unsupported unit '%s'. Supported units are: '%s'" - % (unit, list(self._supported_units.keys())) + % (unit, list(cls._supported_units.keys())) ) def _to_sire_unit(self): diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py index eb715ea5f..0f8dda977 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py @@ -336,24 +336,25 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. unit = unit.replace(" ", "").upper() # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit[:-1] in self._supported_units: + elif unit[:-1] in cls._supported_units: return unit[:-1] - elif unit in self._abbreviations: - return self._abbreviations[unit] - elif unit[:-1] in self._abbreviations: - return self._abbreviations[unit[:-1]] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] + elif unit[:-1] in cls._abbreviations: + return cls._abbreviations[unit[:-1]] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py index bc827b93d..4dad85642 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py @@ -286,7 +286,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -316,13 +317,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "3" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_angle.py b/python/BioSimSpace/Types/_angle.py index b9bf75e6f..cef19c862 100644 --- a/python/BioSimSpace/Types/_angle.py +++ b/python/BioSimSpace/Types/_angle.py @@ -187,7 +187,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -209,13 +210,13 @@ def _validate_unit(self, unit): unit = unit.replace("AD", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_area.py b/python/BioSimSpace/Types/_area.py index 95a0d77e8..2763618a9 100644 --- a/python/BioSimSpace/Types/_area.py +++ b/python/BioSimSpace/Types/_area.py @@ -329,7 +329,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit is supported.""" # Strip whitespace and convert to upper case. @@ -359,13 +360,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "2" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_charge.py b/python/BioSimSpace/Types/_charge.py index 7093b84b9..a65d54cd3 100644 --- a/python/BioSimSpace/Types/_charge.py +++ b/python/BioSimSpace/Types/_charge.py @@ -181,7 +181,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -212,11 +213,11 @@ def _validate_unit(self, unit): unit = unit.replace("COUL", "C") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_energy.py b/python/BioSimSpace/Types/_energy.py index bbdea931e..af9aa2894 100644 --- a/python/BioSimSpace/Types/_energy.py +++ b/python/BioSimSpace/Types/_energy.py @@ -212,7 +212,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -234,11 +235,11 @@ def _validate_unit(self, unit): unit = unit.replace("JOULES", "J") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_length.py b/python/BioSimSpace/Types/_length.py index 907aa9ce7..67f163af8 100644 --- a/python/BioSimSpace/Types/_length.py +++ b/python/BioSimSpace/Types/_length.py @@ -338,7 +338,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -352,13 +353,13 @@ def _validate_unit(self, unit): unit = "ANGS" + unit[3:] # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_pressure.py b/python/BioSimSpace/Types/_pressure.py index 54c958f29..fbe6da782 100644 --- a/python/BioSimSpace/Types/_pressure.py +++ b/python/BioSimSpace/Types/_pressure.py @@ -176,7 +176,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -195,11 +196,11 @@ def _validate_unit(self, unit): unit = unit.replace("S", "") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_temperature.py b/python/BioSimSpace/Types/_temperature.py index 65ab604cc..a7010dd7f 100644 --- a/python/BioSimSpace/Types/_temperature.py +++ b/python/BioSimSpace/Types/_temperature.py @@ -391,7 +391,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -404,16 +405,16 @@ def _validate_unit(self, unit): unit = unit.replace("DEG", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] elif len(unit) == 0: raise ValueError(f"Unit is not given. You must supply the unit.") else: raise ValueError( "Unsupported unit '%s'. Supported units are: '%s'" - % (unit, list(self._supported_units.keys())) + % (unit, list(cls._supported_units.keys())) ) def _to_sire_unit(self): diff --git a/python/BioSimSpace/Types/_time.py b/python/BioSimSpace/Types/_time.py index eb715ea5f..0f8dda977 100644 --- a/python/BioSimSpace/Types/_time.py +++ b/python/BioSimSpace/Types/_time.py @@ -336,24 +336,25 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. unit = unit.replace(" ", "").upper() # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit[:-1] in self._supported_units: + elif unit[:-1] in cls._supported_units: return unit[:-1] - elif unit in self._abbreviations: - return self._abbreviations[unit] - elif unit[:-1] in self._abbreviations: - return self._abbreviations[unit[:-1]] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] + elif unit[:-1] in cls._abbreviations: + return cls._abbreviations[unit[:-1]] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_volume.py b/python/BioSimSpace/Types/_volume.py index bc827b93d..4dad85642 100644 --- a/python/BioSimSpace/Types/_volume.py +++ b/python/BioSimSpace/Types/_volume.py @@ -286,7 +286,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -316,13 +317,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "3" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod From de2a883b294b0f8c364cec415175bc69be70db3e Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 11 Mar 2024 12:58:56 +0000 Subject: [PATCH 037/121] Remove redundant offset variable. --- python/BioSimSpace/Types/_general_unit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/BioSimSpace/Types/_general_unit.py b/python/BioSimSpace/Types/_general_unit.py index 971a39b30..6175c6d38 100644 --- a/python/BioSimSpace/Types/_general_unit.py +++ b/python/BioSimSpace/Types/_general_unit.py @@ -441,7 +441,6 @@ def __pow__(self, other): ) is_inverse = False - offset = 0 if isinstance(other, float): if other > 1: From 0b429ef6232415ecdc8653e9a7e57bda366c380e Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 11 Mar 2024 13:49:20 +0000 Subject: [PATCH 038/121] Windows no longer pulls in requests by default. --- recipes/biosimspace/template.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/recipes/biosimspace/template.yaml b/recipes/biosimspace/template.yaml index 940f55cc1..efcb2c141 100644 --- a/recipes/biosimspace/template.yaml +++ b/recipes/biosimspace/template.yaml @@ -31,6 +31,7 @@ test: - pytest-black # [linux and x86_64 and py==311] - ambertools # [linux and x86_64] - gromacs # [linux and x86_64] + - requests imports: - BioSimSpace source_files: From 3f3e897e5f3aed5a54ffaabf6b4d456954f69623 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 11 Mar 2024 16:53:57 +0000 Subject: [PATCH 039/121] Use decimal representation to simplify __pow__ logic. --- .../Sandpit/Exscientia/Types/_general_unit.py | 60 ++++++++----------- python/BioSimSpace/Types/_general_unit.py | 59 ++++++++---------- 2 files changed, 50 insertions(+), 69 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py index 971a39b30..6add56e0b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py @@ -440,49 +440,39 @@ def __pow__(self, other): % (self.__class__.__qualname__, other.__class__.__qualname__) ) - is_inverse = False - offset = 0 + import math + import decimal - if isinstance(other, float): - if other > 1: - if not other.is_integer(): - raise ValueError("float exponent must be integer valued.") - else: - is_inverse = True - other = 1 / other - if not other.is_integer(): - raise ValueError( - "Divisor in fractional exponent must be integer valued." - ) - other = int(other) + # Convert to a decimal representation. + d = decimal.Decimal(f"{other:.2f}") + numerator, denominator = d.as_integer_ratio() - if other == 0: + if numerator == 0: return GeneralUnit(self._sire_unit / self._sire_unit) - # Get the current unit dimemsions. - dims = self._sire_unit.dimensions() + # Get the existing unit dimensions. + dims = self.dimensions() - # Check that the exponent is a factor of all the unit dimensions. - if is_inverse: - for dim in dims: - if dim % other != 0: - raise ValueError( - "The divisor of the exponent must be a factor of all the unit dimensions." - ) + # First raise to the power of the numerator. + new_dims = [int(dim * numerator) for dim in dims] - if is_inverse: - new_dims = [int(dim / other) for dim in dims] - else: - new_dims = [int(dim * other) for dim in dims] + # Compute the new value. + value = self.value() ** other - if is_inverse: - value = self.value() ** (1 / other) - else: - value = self.value() ** other + if denominator != 1: + # Now check that the denominator is a factor of all the unit dimensions, within + # the accuracy of the decimal representation. + for dim in new_dims: + if dim % denominator != 0: + small = min(dim, denominator) + big = max(dim, denominator) + if small / big < 0.99: + raise ValueError( + "The exponent must be a factor of all the unit dimensions." + ) - # Invert the value. - if other < 0 and not is_inverse: - value = 1 / value + # Divide the dimensions by the denominator. + new_dims = [math.ceil(dim / denominator) for dim in new_dims] # Return a new GeneralUnit object. return GeneralUnit(_GeneralUnit(value, new_dims)) diff --git a/python/BioSimSpace/Types/_general_unit.py b/python/BioSimSpace/Types/_general_unit.py index 6175c6d38..6add56e0b 100644 --- a/python/BioSimSpace/Types/_general_unit.py +++ b/python/BioSimSpace/Types/_general_unit.py @@ -440,48 +440,39 @@ def __pow__(self, other): % (self.__class__.__qualname__, other.__class__.__qualname__) ) - is_inverse = False + import math + import decimal - if isinstance(other, float): - if other > 1: - if not other.is_integer(): - raise ValueError("float exponent must be integer valued.") - else: - is_inverse = True - other = 1 / other - if not other.is_integer(): - raise ValueError( - "Divisor in fractional exponent must be integer valued." - ) - other = int(other) + # Convert to a decimal representation. + d = decimal.Decimal(f"{other:.2f}") + numerator, denominator = d.as_integer_ratio() - if other == 0: + if numerator == 0: return GeneralUnit(self._sire_unit / self._sire_unit) - # Get the current unit dimemsions. - dims = self._sire_unit.dimensions() + # Get the existing unit dimensions. + dims = self.dimensions() - # Check that the exponent is a factor of all the unit dimensions. - if is_inverse: - for dim in dims: - if dim % other != 0: - raise ValueError( - "The divisor of the exponent must be a factor of all the unit dimensions." - ) + # First raise to the power of the numerator. + new_dims = [int(dim * numerator) for dim in dims] - if is_inverse: - new_dims = [int(dim / other) for dim in dims] - else: - new_dims = [int(dim * other) for dim in dims] + # Compute the new value. + value = self.value() ** other - if is_inverse: - value = self.value() ** (1 / other) - else: - value = self.value() ** other + if denominator != 1: + # Now check that the denominator is a factor of all the unit dimensions, within + # the accuracy of the decimal representation. + for dim in new_dims: + if dim % denominator != 0: + small = min(dim, denominator) + big = max(dim, denominator) + if small / big < 0.99: + raise ValueError( + "The exponent must be a factor of all the unit dimensions." + ) - # Invert the value. - if other < 0 and not is_inverse: - value = 1 / value + # Divide the dimensions by the denominator. + new_dims = [math.ceil(dim / denominator) for dim in new_dims] # Return a new GeneralUnit object. return GeneralUnit(_GeneralUnit(value, new_dims)) From 4e8bc6ebe789111cc1901d8c1a41c00ceb337ae9 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 11 Mar 2024 20:31:52 +0000 Subject: [PATCH 040/121] This is a much simpler solution. --- .../Sandpit/Exscientia/Types/_general_unit.py | 40 +++++++------------ python/BioSimSpace/Types/_general_unit.py | 40 +++++++------------ 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py index 6add56e0b..7097e616e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py @@ -440,39 +440,29 @@ def __pow__(self, other): % (self.__class__.__qualname__, other.__class__.__qualname__) ) - import math - import decimal - - # Convert to a decimal representation. - d = decimal.Decimal(f"{other:.2f}") - numerator, denominator = d.as_integer_ratio() - - if numerator == 0: + if other == 0: return GeneralUnit(self._sire_unit / self._sire_unit) + # Convert to float. + other = float(other) + # Get the existing unit dimensions. dims = self.dimensions() - # First raise to the power of the numerator. - new_dims = [int(dim * numerator) for dim in dims] + # Compute the new dimensions, rounding floats to 16 decimal places. + new_dims = [round(dim * other, 16) for dim in dims] - # Compute the new value. - value = self.value() ** other + # Make sure the new dimensions are integers. + if not all(dim.is_integer() for dim in new_dims): + raise ValueError( + "The exponent must be a factor of all the unit dimensions." + ) - if denominator != 1: - # Now check that the denominator is a factor of all the unit dimensions, within - # the accuracy of the decimal representation. - for dim in new_dims: - if dim % denominator != 0: - small = min(dim, denominator) - big = max(dim, denominator) - if small / big < 0.99: - raise ValueError( - "The exponent must be a factor of all the unit dimensions." - ) + # Convert to integers. + new_dims = [int(dim) for dim in new_dims] - # Divide the dimensions by the denominator. - new_dims = [math.ceil(dim / denominator) for dim in new_dims] + # Compute the new value. + value = self.value() ** other # Return a new GeneralUnit object. return GeneralUnit(_GeneralUnit(value, new_dims)) diff --git a/python/BioSimSpace/Types/_general_unit.py b/python/BioSimSpace/Types/_general_unit.py index 6add56e0b..7097e616e 100644 --- a/python/BioSimSpace/Types/_general_unit.py +++ b/python/BioSimSpace/Types/_general_unit.py @@ -440,39 +440,29 @@ def __pow__(self, other): % (self.__class__.__qualname__, other.__class__.__qualname__) ) - import math - import decimal - - # Convert to a decimal representation. - d = decimal.Decimal(f"{other:.2f}") - numerator, denominator = d.as_integer_ratio() - - if numerator == 0: + if other == 0: return GeneralUnit(self._sire_unit / self._sire_unit) + # Convert to float. + other = float(other) + # Get the existing unit dimensions. dims = self.dimensions() - # First raise to the power of the numerator. - new_dims = [int(dim * numerator) for dim in dims] + # Compute the new dimensions, rounding floats to 16 decimal places. + new_dims = [round(dim * other, 16) for dim in dims] - # Compute the new value. - value = self.value() ** other + # Make sure the new dimensions are integers. + if not all(dim.is_integer() for dim in new_dims): + raise ValueError( + "The exponent must be a factor of all the unit dimensions." + ) - if denominator != 1: - # Now check that the denominator is a factor of all the unit dimensions, within - # the accuracy of the decimal representation. - for dim in new_dims: - if dim % denominator != 0: - small = min(dim, denominator) - big = max(dim, denominator) - if small / big < 0.99: - raise ValueError( - "The exponent must be a factor of all the unit dimensions." - ) + # Convert to integers. + new_dims = [int(dim) for dim in new_dims] - # Divide the dimensions by the denominator. - new_dims = [math.ceil(dim / denominator) for dim in new_dims] + # Compute the new value. + value = self.value() ** other # Return a new GeneralUnit object. return GeneralUnit(_GeneralUnit(value, new_dims)) From c9a7f0e8b05729d8b47bd6fbe3e694ae99ffaf63 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 12 Mar 2024 14:42:17 +0000 Subject: [PATCH 041/121] Add custom _find_exe function for AMBER. --- python/BioSimSpace/MD/_md.py | 79 +++++++++---------- python/BioSimSpace/Process/_amber.py | 111 +++++++++++++++++++++++---- tests/Process/test_amber.py | 8 +- 3 files changed, 143 insertions(+), 55 deletions(-) diff --git a/python/BioSimSpace/MD/_md.py b/python/BioSimSpace/MD/_md.py index cc0a9d6f8..e5d3e510c 100644 --- a/python/BioSimSpace/MD/_md.py +++ b/python/BioSimSpace/MD/_md.py @@ -68,7 +68,7 @@ # Whether each engine supports free energy simulations. This dictionary needs to # be updated as support for different engines is added. _free_energy = { - "AMBER": False, + "AMBER": True, "GROMACS": True, "NAMD": False, "OPENMM": False, @@ -96,7 +96,7 @@ } -def _find_md_engines(system, protocol, engine="auto", gpu_support=False): +def _find_md_engines(system, protocol, engine="AUTO", gpu_support=False): """ Find molecular dynamics engines on the system that support the given protocol and GPU requirements. @@ -173,20 +173,45 @@ def _find_md_engines(system, protocol, engine="auto", gpu_support=False): and (not is_metadynamics or _metadynamics[engine]) and (not is_steering or _steering[engine]) ): - # Check whether this engine exists on the system and has the desired - # GPU support. - for exe, gpu in _md_engines[engine].items(): - # If the user has requested GPU support make sure the engine - # supports it. - if not gpu_support or gpu: - # AMBER - if engine == "AMBER": - # Search AMBERHOME, if set. - if _amber_home is not None: - _exe = "%s/bin/%s" % (_amber_home, exe) - if _os.path.isfile(_exe): + # Special handling for AMBER which has a custom executable finding + # function. + if engine == "AMBER": + from ..Process._amber import _find_exe + + try: + exe = _find_exe(is_gpu=gpu_support, is_free_energy=is_free_energy) + found_engines.append(engine) + found_exes.append(exe) + except: + pass + else: + # Check whether this engine exists on the system and has the desired + # GPU support. + for exe, gpu in _md_engines[engine].items(): + # If the user has requested GPU support make sure the engine + # supports it. + if not gpu_support or gpu: + # GROMACS + if engine == "GROMACS": + if ( + _gmx_exe is not None + and _os.path.basename(_gmx_exe) == exe + ): found_engines.append(engine) - found_exes.append(_exe) + found_exes.append(_gmx_exe) + # OPENMM + elif engine == "OPENMM": + found_engines.append(engine) + found_exes.append(_SireBase.getBinDir() + "/sire_python") + # SOMD + elif engine == "SOMD": + found_engines.append(engine) + if is_free_energy: + found_exes.append( + _SireBase.getBinDir() + "/somd-freenrg" + ) + else: + found_exes.append(_SireBase.getBinDir() + "/somd") # Search system PATH. else: try: @@ -195,30 +220,6 @@ def _find_md_engines(system, protocol, engine="auto", gpu_support=False): found_exes.append(exe) except: pass - # GROMACS - elif engine == "GROMACS": - if _gmx_exe is not None and _os.path.basename(_gmx_exe) == exe: - found_engines.append(engine) - found_exes.append(_gmx_exe) - # OPENMM - elif engine == "OPENMM": - found_engines.append(engine) - found_exes.append(_SireBase.getBinDir() + "/sire_python") - # SOMD - elif engine == "SOMD": - found_engines.append(engine) - if is_free_energy: - found_exes.append(_SireBase.getBinDir() + "/somd-freenrg") - else: - found_exes.append(_SireBase.getBinDir() + "/somd") - # Search system PATH. - else: - try: - exe = _SireBase.findExe(exe).absoluteFilePath() - found_engines.append(engine) - found_exes.append(exe) - except: - pass # No engine was found. if len(found_engines) == 0: diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index fb25ecc73..586a04f21 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -73,6 +73,7 @@ def __init__( reference_system=None, explicit_dummies=False, exe=None, + is_gpu=False, name="amber", work_dir=None, seed=None, @@ -103,6 +104,9 @@ def __init__( exe : str The full path to the AMBER executable. + is_gpu : bool + Whether to use the GPU accelerated version of AMBER. + name : str The name of the process. @@ -144,22 +148,18 @@ def __init__( # This process can generate trajectory data. self._has_trajectory = True + if not isinstance(is_gpu, bool): + raise TypeError("'is_gpu' must be of type 'bool'") + # If the path to the executable wasn't specified, then search - # for it in $PATH. For now, we'll just search for 'sander', which - # is available free as part of AmberTools. In future, we will - # look for all possible executables in order of preference: pmemd.cuda, - # pmemd, sander, etc., as well as their variants, e.g. pmemd.MPI. + # for it in AMBERHOME and the PATH. if exe is None: - # Search AMBERHOME, if set. - if _amber_home is not None: - exe = "%s/bin/sander" % _amber_home - if _os.path.isfile(exe): - self._exe = exe - else: - raise _MissingSoftwareError( - "'BioSimSpace.Process.Amber' is not supported. " - "Please install AMBER (http://ambermd.org)." - ) + if isinstance(protocol, _FreeEnergyMixin): + is_free_energy = True + else: + is_free_energy = False + + self._exe = _find_exe(is_gpu=is_gpu, is_free_energy=is_free_energy) else: # Make sure executable exists. if _os.path.isfile(exe): @@ -2703,3 +2703,86 @@ def _get_stdout_record( except KeyError: return None + + +def _find_exe(is_gpu=False, is_free_energy=False): + """ + Helper function to search for an AMBER executable. + + Parameters + ---------- + + is_gpu : bool + Whether to search for a GPU-enabled executable. + + is_free_energy : bool + Whether the executable is for a free energy protocol. + + Returns + ------- + + exe : str + The path to the executable. + """ + + if not isinstance(is_gpu, bool): + raise TypeError("'is_gpu' must be of type 'bool'.") + + if not isinstance(is_free_energy, bool): + raise TypeError("'is_free_energy' must be of type 'bool'.") + + # If the user has requested a GPU-enabled executable, search for pmemd.cuda only. + if is_gpu: + targets = ["pmemd.cuda"] + else: + # If the this is a free energy simulation, then only use pmemd or pmemd.cuda. + if is_free_energy: + targets = ["pmemd"] + else: + targets = ["pmemd", "sander"] + + # Search for the executable. + + import os as _os + import pathlib as _pathlib + + from glob import glob as _glob + + # Get the current path. + path = _os.environ["PATH"].split(_os.pathsep) + + # If AMBERHOME is set, then prepend to the path. + if _amber_home is not None: + path = [_amber_home + "/bin"] + path + + # Helper function to check whether a file is executable. + def is_exe(fpath): + return _os.path.isfile(fpath) and _os.access(fpath, _os.X_OK) + + # Loop over each directory in the path and search for the executable. + for p in path: + # Loop over each target. + for t in targets: + # Glob for the executable. + results = _glob(f"{t}*", root_dir=p) + # If we find a match, check that it's executable and return the path. + # Note that this returns the first match, not the best match. If a + # user requires a specific version of the executable, they should + # order their path accordingly, or use the exe keyword argument. + if results: + for exe in results: + exe = _pathlib.Path(p) / exe + if is_exe(exe): + return str(exe) + + msg = ( + "'BioSimSpace.Process.Amber' is not supported. " + "Unable to find AMBER executable in AMBERHOME or PATH. " + "Please install AMBER (http://ambermd.org)." + ) + + if is_free_energy: + msg += " Free energy simulations require 'pmemd' or 'pmemd.cuda'." + + # If we don't find the executable, raise an error. + raise _MissingSoftwareError(msg) diff --git a/tests/Process/test_amber.py b/tests/Process/test_amber.py index 600b4ff81..bd7a8a703 100644 --- a/tests/Process/test_amber.py +++ b/tests/Process/test_amber.py @@ -314,11 +314,15 @@ def run_process(system, protocol, check_data=False): def test_parse_fep_output(perturbable_system, protocol): """Make sure that we can correctly parse AMBER FEP output.""" + from sire.legacy.Base import findExe + # Copy the system. system_copy = perturbable_system.copy() - # Create a process using any system and the protocol. - process = BSS.Process.Amber(system_copy, protocol) + # Use the first instance of sander in the path so that we can + # test without pmemd. + exe = findExe("sander").absoluteFilePath() + process = BSS.Process.Amber(system_copy, protocol, exe=exe) # Assign the path to the output file. if isinstance(protocol, BSS.Protocol.FreeEnergy): From 004dbb3aa0d897fb453ac8b431a32f701cd7c4d4 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 12 Mar 2024 15:40:45 +0000 Subject: [PATCH 042/121] Appears that igb=6 is only needed for pmemd.cuda. --- python/BioSimSpace/Process/_amber.py | 10 +++++----- python/BioSimSpace/_Config/_amber.py | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index 586a04f21..5b9bda3c9 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -299,11 +299,11 @@ def _setup(self): def _generate_config(self): """Generate AMBER configuration file strings.""" - # Work out whether we're generating a config for PMEMD. - if "pmemd" in self._exe.lower(): - is_pmemd = True + # Is this a CUDA enabled version of AMBER? + if "cuda" in self._exe.lower(): + is_pmemd_cuda = True else: - is_pmemd = False + is_pmemd_cuda = False extra_options = self._extra_options.copy() extra_lines = self._extra_lines.copy() @@ -346,7 +346,7 @@ def _generate_config(self): # Create the configuration. self.setConfig( amber_config.createConfig( - is_pmemd=is_pmemd, + is_pmemd_cuda=is_pmemd_cuda, explicit_dummies=self._explicit_dummies, extra_options=extra_options, extra_lines=extra_lines, diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index d92eb1145..d8de5295f 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -68,6 +68,7 @@ def createConfig( self, version=None, is_pmemd=False, + is_pmemd_cuda=False, explicit_dummies=False, extra_options={}, extra_lines=[], @@ -78,8 +79,8 @@ def createConfig( version : float The AMBER version. - is_pmemd : bool - Whether the configuration is for a simulation using PMEMD. + is_pmemd_cuda : bool + Whether the configuration is for a simulation using PMEMD with CUDA. explicit_dummies : bool Whether to keep the dummy atoms explicit at the endstates or remove them. @@ -103,8 +104,8 @@ def createConfig( if version and not isinstance(version, float): raise TypeError("'version' must be of type 'float'.") - if not isinstance(is_pmemd, bool): - raise TypeError("'is_pmemd' must be of type 'bool'.") + if not isinstance(is_pmemd_cuda, bool): + raise TypeError("'is_pmemd_cuda' must be of type 'bool'.") if not isinstance(explicit_dummies, bool): raise TypeError("'explicit_dummies' must be of type 'bool'.") @@ -192,7 +193,7 @@ def createConfig( protocol_dict["ntb"] = 0 # Non-bonded cut-off. protocol_dict["cut"] = "999." - if is_pmemd: + if is_pmemd_cuda: # Use vacuum generalised Born model. protocol_dict["igb"] = "6" else: From cdc962c82d848052cd71db4391769f7e2222fa93 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 12 Mar 2024 15:41:27 +0000 Subject: [PATCH 043/121] Remove invalid kwarg. --- python/BioSimSpace/FreeEnergy/_relative.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/BioSimSpace/FreeEnergy/_relative.py b/python/BioSimSpace/FreeEnergy/_relative.py index ee1af9e7b..a0f04741e 100644 --- a/python/BioSimSpace/FreeEnergy/_relative.py +++ b/python/BioSimSpace/FreeEnergy/_relative.py @@ -2076,7 +2076,6 @@ def _initialise_runner(self, system): first_process = _Process.Amber( system, self._protocol, - exe=self._exe, work_dir=first_dir, extra_options=self._extra_options, extra_lines=self._extra_lines, From 5b68d729aaa1e6edd198d66a2df6f64ee4b2ca2f Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 12 Mar 2024 16:18:43 +0000 Subject: [PATCH 044/121] Append process object for first lambda window. --- python/BioSimSpace/FreeEnergy/_relative.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/BioSimSpace/FreeEnergy/_relative.py b/python/BioSimSpace/FreeEnergy/_relative.py index a0f04741e..eb2889d9b 100644 --- a/python/BioSimSpace/FreeEnergy/_relative.py +++ b/python/BioSimSpace/FreeEnergy/_relative.py @@ -2082,6 +2082,10 @@ def _initialise_runner(self, system): property_map=self._property_map, **self._kwargs, ) + if self._setup_only: + del first_process + else: + processes.append(first_process) # Loop over the rest of the lambda values. for x, lam in enumerate(lam_vals[1:]): From baf0aefba6943ef315cb44a79edfa9fe359af5eb Mon Sep 17 00:00:00 2001 From: Zhiyi Wu Date: Tue, 12 Mar 2024 20:34:29 +0000 Subject: [PATCH 045/121] =?UTF-8?q?Implement=20the=20Schr=C3=B6dinger's=20?= =?UTF-8?q?derivation=20of=20the=20analytical=20correction=20for=20Boresch?= =?UTF-8?q?=20restraint=20(#40)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Exscientia/FreeEnergy/_restraint.py | 250 +++++++++++++----- .../Exscientia/FreeEnergy/test_restraint.py | 13 +- 2 files changed, 189 insertions(+), 74 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py index 74d9652ee..5aaa857db 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py @@ -22,29 +22,54 @@ """ A class for holding restraints. """ -from math import e -from scipy import integrate as _integrate -import numpy as _np +import math as _math import warnings as _warnings +from typing import Literal -from sire.legacy.Units import angstrom3 as _Sire_angstrom3 -from sire.legacy.Units import k_boltz as _k_boltz -from sire.legacy.Units import meter3 as _Sire_meter3 -from sire.legacy.Units import mole as _Sire_mole - -from .._SireWrappers import Atom as _Atom -from .._SireWrappers import System as _System -from ..Types import Angle as _Angle -from ..Types import Length as _Length -from ..Types import Temperature as _Temperature -from ..Units.Angle import degree as _degree -from ..Units.Angle import radian as _radian +import numpy as _np +from scipy import integrate as _integrate +from scipy.special import erf as _erf +from sire.legacy.Units import ( + angstrom3 as _Sire_angstrom3, + k_boltz as _k_boltz, + meter3 as _Sire_meter3, + mole as _Sire_mole, +) +from sire.units import GeneralUnit as _sire_GeneralUnit + +from BioSimSpace.Sandpit.Exscientia.Types._general_unit import ( + GeneralUnit as _GeneralUnit, +) +from ..Types import Angle as _Angle, Length as _Length, Temperature as _Temperature +from ..Units.Angle import degree as _degree, radian as _radian from ..Units.Area import angstrom2 as _angstrom2 -from ..Units.Energy import kcal_per_mol as _kcal_per_mol -from ..Units.Energy import kj_per_mol as _kj_per_mol -from ..Units.Length import angstrom as _angstrom -from ..Units.Length import nanometer as _nanometer +from ..Units.Energy import kcal_per_mol as _kcal_per_mol, kj_per_mol as _kj_per_mol +from ..Units.Length import angstrom as _angstrom, nanometer as _nanometer from ..Units.Temperature import kelvin as _kelvin +from ..Units.Volume import angstrom3 as _angstrom3 +from .._SireWrappers import Atom as _Atom, System as _System + + +def sqrt(u): + dims = u._sire_unit.dimensions() + for dim in dims: + if dim % 2 != 0: + raise ValueError( + "Square root not possible on dimension that is not divisible by 2!" + ) + return _GeneralUnit( + _sire_GeneralUnit(_math.sqrt(u.value()), [int(0.5 * dim) for dim in dims]) + ) + + +def exp(u): + dims = u._sire_unit.dimensions() + return _GeneralUnit(_sire_GeneralUnit(_math.exp(u.value()), dims)) + + +def erf(u): + dims = u._sire_unit.dimensions() + return _GeneralUnit(_sire_GeneralUnit(_erf(u.value()), dims)) class Restraint: @@ -813,7 +838,11 @@ def toString(self, engine, perturbation_type=None, restraint_lambda=False): f"yet for {engine}." ) - def getCorrection(self, method="analytical"): + def getCorrection( + self, + method="analytical", + flavour: Literal["boresch", "schrodinger"] = "boresch", + ): """ Calculate the free energy of releasing the restraint to the standard state volume. @@ -827,6 +856,11 @@ def getCorrection(self, method="analytical"): correction can introduce errors when the restraints are weak, restrained angles are close to 0 or pi radians, or the restrained distance is close to 0. + flavour : str + When analytical correction is used, one could either use + Boresch's derivation or Schrodinger's derivation. Both of + them usually agrees quite well with each other to the extent + of 0.2 kcal/mol. Returns ---------- @@ -975,59 +1009,10 @@ def numerical_dihedral_integrand(phi, phi0, kphi): return dg elif method == "analytical": - # Only need three equilibrium values for the analytical correction - r0 = ( - self._restraint_dict["equilibrium_values"]["r0"] / _angstrom - ) # Distance in A - thetaA0 = ( - self._restraint_dict["equilibrium_values"]["thetaA0"] / _radian - ) # Angle in radians - thetaB0 = ( - self._restraint_dict["equilibrium_values"]["thetaB0"] / _radian - ) # Angle in radians - - force_constants = [] - - # Loop through and correct for force constants of zero, - # which break the analytical correction. To account for this, - # divide the prefactor accordingly. Note that setting - # certain force constants to zero while others are non-zero - # will result in unstable restraints, but this will be checked when - # the restraint object is created - for k, val in self._restraint_dict["force_constants"].items(): - if val.value() == 0: - if k == "kr": - raise ValueError("The force constant kr must not be zero") - if k == "kthetaA": - prefactor /= 2 / _np.sin(thetaA0) - if k == "kthetaB": - prefactor /= 2 / _np.sin(thetaB0) - if k[:4] == "kphi": - prefactor /= 2 * _np.pi - else: - if k == "kr": - force_constants.append(val / (_kcal_per_mol / _angstrom2)) - else: - force_constants.append( - val / (_kcal_per_mol / (_radian * _radian)) - ) - - # Calculation - n_nonzero_k = len(force_constants) - prod_force_constants = _np.prod(force_constants) - numerator = prefactor * _np.sqrt(prod_force_constants) - denominator = ( - (r0**2) - * _np.sin(thetaA0) - * _np.sin(thetaB0) - * (2 * _np.pi * R * T) ** (n_nonzero_k / 2) - ) - - # Compute dg and attach unit - dg = -R * T * _np.log(numerator / denominator) - dg *= _kcal_per_mol - - return dg + if flavour.lower() == "schrodinger": + return self._schrodinger_analytical_correction() + elif flavour.lower() == "boresch": + return self._boresch_analytical_correction() else: raise ValueError( @@ -1127,6 +1112,125 @@ def _get_correction(r0, r_fb, kr): ) return _get_correction(r0, r_fb, kr) + def _schrodinger_analytical_correction(self): + # Adapted from DOI: 10.1021/acs.jcim.3c00013 + k_boltz = _GeneralUnit(_k_boltz) + beta = 1 / (k_boltz * self.T) + V = 1660 * _angstrom3 + + r = self._restraint_dict["equilibrium_values"]["r0"] + # Schrodinger uses k(b-b0)**2 + kr = self._restraint_dict["force_constants"]["kr"] / 2 + + Z_dist = r / (2 * beta * kr) * _np.exp(-beta * kr * r**2) + _np.sqrt( + _np.pi + ) / (4 * beta * kr * sqrt(beta * kr)) * (1 + 2 * beta * kr * r**2) * ( + 1 + _erf(sqrt(beta * kr) * r) + ) + + Z_angles = [] + for angle in ["A", "B"]: + theta = ( + self._restraint_dict["equilibrium_values"][f"theta{angle}0"] / _radian + ) # Angle in radians + # Schrodinger uses k instead of k/2 + ktheta = self._restraint_dict["force_constants"][f"ktheta{angle}"] / 2 + Z_angle = ( + sqrt(_np.pi / (beta * ktheta)) + * exp(-1 / (4 * beta * ktheta)) + * _np.sin(theta) + ) + Z_angle /= _radian**3 + Z_angles.append(Z_angle) + + Z_dihedrals = [] + for dihedral in ["A", "B", "C"]: + # Schrodinger uses k instead of k/2 + kphi = self._restraint_dict["force_constants"][f"kphi{dihedral}"] / 2 + Z_dihedral = sqrt(_np.pi / (beta * kphi)) * erf(_np.pi * sqrt(beta * kphi)) + Z_dihedrals.append(Z_dihedral) + + dG = ( + k_boltz + * self.T + * _np.log( + Z_angles[0] + * Z_angles[1] + * Z_dist + * Z_dihedrals[0] + * Z_dihedrals[1] + * Z_dihedrals[2] + / (8 * _np.pi**2 * V) + ) + ) + return dG + + def _boresch_analytical_correction(self): + R = ( + _k_boltz.value() * _kcal_per_mol / _kelvin + ).value() # molar gas constant in kcal mol-1 K-1 + + # Parameters + T = self.T / _kelvin # Temperature in Kelvin + v0 = ( + ((_Sire_meter3 / 1000) / _Sire_mole) / _Sire_angstrom3 + ).value() # standard state volume in A^3 + prefactor = ( + 8 * (_np.pi**2) * v0 + ) # In A^3. Divide this to account for force constants of 0 in the + # analytical correction + # Only need three equilibrium values for the analytical correction + r0 = ( + self._restraint_dict["equilibrium_values"]["r0"] / _angstrom + ) # Distance in A + thetaA0 = ( + self._restraint_dict["equilibrium_values"]["thetaA0"] / _radian + ) # Angle in radians + thetaB0 = ( + self._restraint_dict["equilibrium_values"]["thetaB0"] / _radian + ) # Angle in radians + + force_constants = [] + + # Loop through and correct for force constants of zero, + # which break the analytical correction. To account for this, + # divide the prefactor accordingly. Note that setting + # certain force constants to zero while others are non-zero + # will result in unstable restraints, but this will be checked when + # the restraint object is created + for k, val in self._restraint_dict["force_constants"].items(): + if val.value() == 0: + if k == "kr": + raise ValueError("The force constant kr must not be zero") + if k == "kthetaA": + prefactor /= 2 / _np.sin(thetaA0) + if k == "kthetaB": + prefactor /= 2 / _np.sin(thetaB0) + if k[:4] == "kphi": + prefactor /= 2 * _np.pi + else: + if k == "kr": + force_constants.append(val / (_kcal_per_mol / _angstrom2)) + else: + force_constants.append(val / (_kcal_per_mol / (_radian * _radian))) + + # Calculation + n_nonzero_k = len(force_constants) + prod_force_constants = _np.prod(force_constants) + numerator = prefactor * _np.sqrt(prod_force_constants) + denominator = ( + (r0**2) + * _np.sin(thetaA0) + * _np.sin(thetaB0) + * (2 * _np.pi * R * T) ** (n_nonzero_k / 2) + ) + + # Compute dg and attach unit + dg = -R * T * _np.log(numerator / denominator) + dg *= _kcal_per_mol + + return dg + @property def correction(self): """Give the free energy of removing the restraint.""" diff --git a/tests/Sandpit/Exscientia/FreeEnergy/test_restraint.py b/tests/Sandpit/Exscientia/FreeEnergy/test_restraint.py index 6c88cb050..0149a64e3 100644 --- a/tests/Sandpit/Exscientia/FreeEnergy/test_restraint.py +++ b/tests/Sandpit/Exscientia/FreeEnergy/test_restraint.py @@ -97,11 +97,22 @@ def test_numerical_correction_boresch(boresch_restraint): def test_analytical_correction_boresch(boresch_restraint): - dG = boresch_restraint.getCorrection(method="analytical") / kcal_per_mol + dG = ( + boresch_restraint.getCorrection(method="analytical", flavour="boresch") + / kcal_per_mol + ) assert np.isclose(-7.2, dG, atol=0.1) assert isinstance(boresch_restraint, Restraint) +def test_analytical_schrodinger_correction_boresch(boresch_restraint): + dG = ( + boresch_restraint.getCorrection(method="analytical", flavour="schrodinger") + / kcal_per_mol + ) + assert np.isclose(-7.2, dG, atol=0.1) + + test_force_constants_boresch = [ ({"kr": 0}, ValueError), ({"kthetaA": 0}, ValueError), From 34e2730d83b944ac37c92605e68033938dfaa0a1 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 13 Mar 2024 12:38:30 +0000 Subject: [PATCH 046/121] Add way of specifying extra command-line arguments for AMBER and GROMACS --- python/BioSimSpace/FreeEnergy/_relative.py | 75 +--------------------- python/BioSimSpace/Process/_amber.py | 9 +++ python/BioSimSpace/Process/_gromacs.py | 14 ++++ python/BioSimSpace/Process/_process.py | 19 +++++- 4 files changed, 42 insertions(+), 75 deletions(-) diff --git a/python/BioSimSpace/FreeEnergy/_relative.py b/python/BioSimSpace/FreeEnergy/_relative.py index eb2889d9b..a54a6d707 100644 --- a/python/BioSimSpace/FreeEnergy/_relative.py +++ b/python/BioSimSpace/FreeEnergy/_relative.py @@ -135,10 +135,6 @@ def __init__( work_dir=None, engine=None, setup_only=False, - ignore_warnings=False, - show_errors=True, - extra_options={}, - extra_lines=[], property_map={}, **kwargs, ): @@ -177,23 +173,6 @@ def __init__( can be useful when you don't intend to use BioSimSpace to run the simulation. Note that a 'work_dir' must also be specified. - ignore_warnings : bool - Whether to ignore warnings when generating the binary run file. - This option is specific to GROMACS and will be ignored when a - different molecular dynamics engine is chosen. - - show_errors : bool - Whether to show warning/error messages when generating the binary - run file. This option is specific to GROMACS and will be ignored - when a different molecular dynamics engine is chosen. - - extra_options : dict - A dictionary containing extra options. Overrides the defaults generated - by the protocol. - - extra_lines : [str] - A list of extra lines to put at the end of the configuration file. - property_map : dict A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their @@ -293,31 +272,6 @@ def __init__( # Create the working directory. self._work_dir = _Utils.WorkDir(work_dir) - if not isinstance(ignore_warnings, bool): - raise ValueError("'ignore_warnings' must be of type 'bool.") - self._ignore_warnings = ignore_warnings - - if not isinstance(show_errors, bool): - raise ValueError("'show_errors' must be of type 'bool.") - self._show_errors = show_errors - - # Check the extra options. - if not isinstance(extra_options, dict): - raise TypeError("'extra_options' must be of type 'dict'.") - else: - keys = extra_options.keys() - if not all(isinstance(k, str) for k in keys): - raise TypeError("Keys of 'extra_options' must be of type 'str'.") - self._extra_options = extra_options - - # Check the extra lines. - if not isinstance(extra_lines, list): - raise TypeError("'extra_lines' must be of type 'list'.") - else: - if not all(isinstance(line, str) for line in extra_lines): - raise TypeError("Lines in 'extra_lines' must be of type 'str'.") - self._extra_lines = extra_lines - # Check that the map is valid. if not isinstance(property_map, dict): raise TypeError("'property_map' must be of type 'dict'") @@ -2045,8 +1999,6 @@ def _initialise_runner(self, system): self._protocol, platform=platform, work_dir=first_dir, - extra_options=self._extra_options, - extra_lines=self._extra_lines, property_map=self._property_map, **self._kwargs, ) @@ -2061,10 +2013,6 @@ def _initialise_runner(self, system): system, self._protocol, work_dir=first_dir, - ignore_warnings=self._ignore_warnings, - show_errors=self._show_errors, - extra_options=self._extra_options, - extra_lines=self._extra_lines, property_map=self._property_map, **self._kwargs, ) @@ -2077,8 +2025,6 @@ def _initialise_runner(self, system): system, self._protocol, work_dir=first_dir, - extra_options=self._extra_options, - extra_lines=self._extra_lines, property_map=self._property_map, **self._kwargs, ) @@ -2173,8 +2119,7 @@ def _initialise_runner(self, system): gro, tpr, first_process._exe, - ignore_warnings=self._ignore_warnings, - show_errors=self._show_errors, + **self._kwargs, ) # Create a copy of the process and update the working @@ -2240,24 +2185,6 @@ def _initialise_runner(self, system): # inside the working directory so no need to re-nest. self._runner = _Process.ProcessRunner(processes) - def _update_run_args(self, args): - """ - Internal function to update run arguments for all subprocesses. - - Parameters - ---------- - - args : dict, collections.OrderedDict - A dictionary which contains the new command-line arguments - for the process executable. - """ - - if not isinstance(args, dict): - raise TypeError("'args' must be of type 'dict'") - - for process in self._runner.processes(): - process.setArgs(args) - def getData(name="data", file_link=False, work_dir=None): """ diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index 5b9bda3c9..eddc02107 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -79,6 +79,7 @@ def __init__( seed=None, extra_options={}, extra_lines=[], + extra_args={}, property_map={}, ): """ @@ -123,6 +124,9 @@ def __init__( extra_lines : [str] A list of extra lines to put at the end of the configuration file. + extra_args : dict + A dictionary of extra command-line arguments to pass to the AMBER executable. + property_map : dict A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their @@ -139,6 +143,7 @@ def __init__( seed=seed, extra_options=extra_options, extra_lines=extra_lines, + extra_args=extra_args, property_map=property_map, ) @@ -383,6 +388,10 @@ def _generate_args(self): if not isinstance(self._protocol, _Protocol.Minimisation): self.setArg("-x", "%s.nc" % self._name) + # Add the extra arguments. + for key, value in self._extra_args.items(): + self.setArg(key, value) + def start(self): """ Start the AMBER process. diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index 7c22f60e0..c4e6e36a9 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -83,6 +83,7 @@ def __init__( seed=None, extra_options={}, extra_lines=[], + extra_args={}, property_map={}, ignore_warnings=False, show_errors=True, @@ -124,6 +125,10 @@ def __init__( extra_lines : [str] A list of extra lines to put at the end of the configuration file. + extra_args : dict + A dictionary of extra command-line arguments to pass to the GROMACS + executable. + property_map : dict A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their @@ -154,6 +159,7 @@ def __init__( seed=seed, extra_options=extra_options, extra_lines=extra_lines, + extra_args=extra_args, property_map=property_map, ) @@ -444,6 +450,10 @@ def _generate_args(self): if isinstance(self._protocol, (_Protocol.Metadynamics, _Protocol.Steering)): self.setArg("-plumed", "plumed.dat") + # Add any extra arguments. + for key, value in self._extra_args.items(): + self.setArg(key, value) + @staticmethod def _generate_binary_run_file( mdp_file, @@ -455,6 +465,7 @@ def _generate_binary_run_file( checkpoint_file=None, ignore_warnings=False, show_errors=True, + **kwargs, ): """ Use grommp to generate the binary run input file. @@ -494,6 +505,9 @@ def _generate_binary_run_file( show_errors : bool Whether to show warning/error messages when generating the binary run file. + + **kwargs : dict + Additional keyword arguments. """ if not isinstance(mdp_file, str): diff --git a/python/BioSimSpace/Process/_process.py b/python/BioSimSpace/Process/_process.py index 1865b128d..a4ef0ebf2 100644 --- a/python/BioSimSpace/Process/_process.py +++ b/python/BioSimSpace/Process/_process.py @@ -76,6 +76,7 @@ def __init__( seed=None, extra_options={}, extra_lines=[], + extra_args={}, property_map={}, ): """ @@ -115,6 +116,9 @@ def __init__( extra_lines : [str] A list of extra lines to put at the end of the configuration file. + extra_args : dict + A dictionary containing extra command-line arguments. + property_map : dict A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their @@ -189,6 +193,14 @@ def __init__( if not all(isinstance(line, str) for line in extra_lines): raise TypeError("Lines in 'extra_lines' must be of type 'str'.") + # Check the extra arguments. + if not isinstance(extra_args, dict): + raise TypeError("'extra_args' must be of type 'dict'.") + else: + keys = extra_args.keys() + if not all(isinstance(k, str) for k in keys): + raise TypeError("Keys of 'extra_args' must be of type 'str'.") + # Check that the map is valid. if not isinstance(property_map, dict): raise TypeError("'property_map' must be of type 'dict'") @@ -244,9 +256,10 @@ def __init__( self._is_seeded = True self.setSeed(seed) - # Set the extra options and lines. + # Set the extra options, lines, and args. self._extra_options = extra_options self._extra_lines = extra_lines + self._extra_args = extra_args # Set the map. self._property_map = property_map.copy() @@ -1461,6 +1474,10 @@ def setArgs(self, args): "'args' must be of type 'dict' or 'collections.OrderedDict'" ) + # Add extra arguments. + if self._extra_args: + self.addArgs(self._extra_args) + def setArg(self, arg, value): """ Set a specific command-line argument. From d01f254ea8f2a542ac0d3e11d42d72bbe167e7e4 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 13 Mar 2024 12:43:32 +0000 Subject: [PATCH 047/121] Add **kwargs to all Process classes. --- python/BioSimSpace/Process/_amber.py | 4 ++++ python/BioSimSpace/Process/_namd.py | 4 ++++ python/BioSimSpace/Process/_openmm.py | 4 ++++ python/BioSimSpace/Process/_somd.py | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index eddc02107..4bf4a5f1c 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -81,6 +81,7 @@ def __init__( extra_lines=[], extra_args={}, property_map={}, + **kwargs, ): """ Constructor. @@ -131,6 +132,9 @@ def __init__( A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their own naming scheme, e.g. { "charge" : "my-charge" } + + kwargs : dict + Additional keyword arguments. """ # Call the base class constructor. diff --git a/python/BioSimSpace/Process/_namd.py b/python/BioSimSpace/Process/_namd.py index f996c9555..c50165947 100644 --- a/python/BioSimSpace/Process/_namd.py +++ b/python/BioSimSpace/Process/_namd.py @@ -69,6 +69,7 @@ def __init__( work_dir=None, seed=None, property_map={}, + **kwargs, ): """ Constructor. @@ -103,6 +104,9 @@ def __init__( A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their own naming scheme, e.g. { "charge" : "my-charge" } + + kwargs : dict + Additional keyword arguments. """ # Call the base class constructor. diff --git a/python/BioSimSpace/Process/_openmm.py b/python/BioSimSpace/Process/_openmm.py index 76bb8451a..66c461495 100644 --- a/python/BioSimSpace/Process/_openmm.py +++ b/python/BioSimSpace/Process/_openmm.py @@ -79,6 +79,7 @@ def __init__( work_dir=None, seed=None, property_map={}, + **kwargs, ): """ Constructor. @@ -120,6 +121,9 @@ def __init__( A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their own naming scheme, e.g. { "charge" : "my-charge" } + + kwargs : dict + Additional keyword arguments. """ # Call the base class constructor. diff --git a/python/BioSimSpace/Process/_somd.py b/python/BioSimSpace/Process/_somd.py index 885afa398..c64f8e689 100644 --- a/python/BioSimSpace/Process/_somd.py +++ b/python/BioSimSpace/Process/_somd.py @@ -79,6 +79,7 @@ def __init__( extra_options={}, extra_lines=[], property_map={}, + **kwargs, ): """ Constructor. @@ -121,6 +122,9 @@ def __init__( A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their own naming scheme, e.g. { "charge" : "my-charge" } + + kwargs : dict + Additional keyword arguments. """ # Call the base class constructor. From 18b95cb3e22e1d829832c8321f0c837b61a6e3cb Mon Sep 17 00:00:00 2001 From: Zhiyi Wu Date: Wed, 13 Mar 2024 13:38:36 +0000 Subject: [PATCH 048/121] Have the position restraint deals with the alchemical ion (#39) --- .../Sandpit/Exscientia/Align/_alch_ion.py | 22 ++- .../Sandpit/Exscientia/Process/_amber.py | 14 +- .../Sandpit/Exscientia/Process/_gromacs.py | 58 +++++-- .../Sandpit/Exscientia/Protocol/_config.py | 153 ++++++++++-------- .../Exscientia/_SireWrappers/_molecule.py | 38 +++-- .../Exscientia/Align/test_alchemical_ion.py | 13 +- .../Sandpit/Exscientia/Process/test_amber.py | 4 +- .../Exscientia/Process/test_gromacs.py | 13 +- .../Process/test_position_restraint.py | 127 ++++++++++++++- 9 files changed, 327 insertions(+), 115 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Align/_alch_ion.py b/python/BioSimSpace/Sandpit/Exscientia/Align/_alch_ion.py index 14773adf6..cd75db2a3 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Align/_alch_ion.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Align/_alch_ion.py @@ -1,6 +1,6 @@ import warnings -from .._SireWrappers import Molecule as _Molecule +from .._SireWrappers import Molecule as _Molecule, System as _System def _mark_alchemical_ion(molecule): @@ -50,3 +50,23 @@ def _mark_alchemical_ion(molecule): mol._sire_object = mol_edit.commit() return mol + + +def _get_protein_com_idx(system: _System) -> int: + """return the index of the atom that is closest to the center of + mass of the biggest molecule in the system. + + Args: + system: The input system. + + Returns: + atom_index + """ + biggest_mol_idx = max(range(system.nMolecules()), key=lambda x: system[x].nAtoms()) + + atom_offset = 0 + for i, mol in enumerate(system): + if biggest_mol_idx == i: + return atom_offset + mol.getCOMIdx() + else: + atom_offset += mol.nAtoms() diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py index d84e7eaa5..f70f4f13b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_amber.py @@ -44,7 +44,6 @@ from sire.legacy import Base as _SireBase from sire.legacy import IO as _SireIO from sire.legacy import Mol as _SireMol -import pandas as pd from .._Utils import _assert_imported, _have_imported, _try_import @@ -236,8 +235,12 @@ def _setup(self): ) # Create the reference file - if self._ref_system is not None and self._protocol.getRestraint() is not None: - self._write_system(self._ref_system, ref_file=self._ref_file) + if self._ref_system is not None: + if ( + self._system.getAlchemicalIon() + or self._protocol.getRestraint() is not None + ): + self._write_system(self._ref_system, ref_file=self._ref_file) else: _shutil.copy(self._rst_file, self._ref_file) @@ -543,7 +546,10 @@ def _generate_args(self): if not isinstance(self._protocol, _Protocol.Custom): # Append a reference file if this a restrained simulation. if isinstance(self._protocol, _Protocol._PositionRestraintMixin): - if self._protocol.getRestraint() is not None: + if ( + self._protocol.getRestraint() is not None + or self._system.getAlchemicalIon() + ): self.setArg("-ref", "%s_ref.rst7" % self._name) # Append a trajectory file if this anything other than a minimisation. diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py index 2586be104..5953781fc 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py @@ -28,9 +28,6 @@ import glob as _glob import os as _os -import warnings as _warnings - -import pandas as pd from .._Utils import _try_import @@ -261,8 +258,12 @@ def _setup(self): ) # Create the reference file - if self._ref_system is not None and self._protocol.getRestraint() is not None: - self._write_system(self._ref_system, ref_file=self._ref_file) + if self._ref_system is not None: + if ( + self._system.getAlchemicalIon() + or self._protocol.getRestraint() is not None + ): + self._write_system(self._ref_system, ref_file=self._ref_file) else: _shutil.copy(self._gro_file, self._ref_file) @@ -2080,7 +2081,7 @@ def _add_position_restraints(self, config_options): # Get the restraint type. restraint = self._protocol.getRestraint() - if restraint is not None: + if restraint is not None or self._system.getAlchemicalIon(): # Get the force constant in units of kJ_per_mol/nanometer**2 force_constant = self._protocol.getForceConstant()._sire_unit force_constant = force_constant.to( @@ -2140,8 +2141,15 @@ def _add_position_restraints(self, config_options): moltypes_sys_idx[mol_type].append(idx) sys_idx_moltypes[idx] = mol_type + if self._system.getAlchemicalIon(): + biggest_mol_idx = max( + range(system.nMolecules()), key=lambda x: system[x].nAtoms() + ) + else: + biggest_mol_idx = -1 + # A keyword restraint. - if isinstance(restraint, str): + if isinstance(restraint, str) or self._system.getAlchemicalIon(): # The number of restraint files. num_restraint = 1 @@ -2158,12 +2166,36 @@ def _add_position_restraints(self, config_options): for idx, mol_idx in enumerate(mol_idxs): # Get the indices of any restrained atoms in this molecule, # making sure that indices are relative to the molecule. - atom_idxs = self._system.getRestraintAtoms( - restraint, - mol_index=mol_idx, - is_absolute=False, - allow_zero_matches=True, - ) + if restraint is not None: + atom_idxs = self._system.getRestraintAtoms( + restraint, + mol_index=mol_idx, + is_absolute=False, + allow_zero_matches=True, + ) + else: + atom_idxs = [] + + if self._system.getMolecule(mol_idx).isAlchemicalIon(): + alch_ion = self._system.getMolecule(mol_idx).getAtoms() + alch_idx = alch_ion[0].index() + if alch_idx != 0 or len(alch_ion) != 1: + # The alchemical ions should only contain 1 atom + # and the relative index should thus be 0. + raise ValueError( + f"{self._system.getMolecule(mol_idx)} is marked as an alchemical ion but has more than 1 atom." + ) + else: + atom_idxs.append(alch_idx) + + if mol_idx == biggest_mol_idx: + # Only triggered when there is alchemical ion present. + # The biggest_mol_idx is -1 when there is no alchemical ion. + protein_com_idx = self._system.getMolecule( + mol_idx + ).getCOMIdx() + if protein_com_idx not in atom_idxs: + atom_idxs.append(protein_com_idx) # Store the atom index if it hasn't already been recorded. for atom_idx in atom_idxs: diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py index a0319ea2b..99446e14b 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py @@ -1,16 +1,15 @@ +import math as _math import warnings as _warnings -import math as _math from sire.legacy import Units as _SireUnits -from ..Units.Time import nanosecond as _nanosecond -from .. import Protocol as _Protocol -from .. import _gmx_version -from .._Exceptions import IncompatibleError as _IncompatibleError +from .. import Protocol as _Protocol, _gmx_version +from ..Align._alch_ion import _get_protein_com_idx from ..Align._squash import _amber_mask_from_indices, _squashed_atom_mapping from ..FreeEnergy._restraint import Restraint as _Restraint from ..Units.Energy import kj_per_mol as _kj_per_mol from ..Units.Length import nanometer as _nanometer +from .._Exceptions import IncompatibleError as _IncompatibleError class ConfigFactory: @@ -217,9 +216,9 @@ def generateAmberConfig(self, extra_options=None, extra_lines=None): protocol_dict["imin"] = 1 # Minimisation simulation. protocol_dict["ntmin"] = 2 # Set the minimisation method to XMIN protocol_dict["maxcyc"] = self._steps # Set the number of steps. - protocol_dict[ - "ncyc" - ] = num_steep # Set the number of steepest descent steps. + protocol_dict["ncyc"] = ( + num_steep # Set the number of steepest descent steps. + ) # FIX need to remove and fix this, only for initial testing timestep = 0.004 else: @@ -248,6 +247,13 @@ def generateAmberConfig(self, extra_options=None, extra_lines=None): # Restrain the backbone. restraint = self.protocol.getRestraint() + if self.system.getAlchemicalIon(): + alchem_ion_idx = self.system.getAlchemicalIonIdx() + protein_com_idx = _get_protein_com_idx(self.system) + alchemical_ion_mask = f"@{alchem_ion_idx} | @{protein_com_idx}" + else: + alchemical_ion_mask = None + if restraint is not None: # Get the indices of the atoms that are restrained. if type(restraint) is str: @@ -293,9 +299,9 @@ def generateAmberConfig(self, extra_options=None, extra_lines=None): ] restraint_mask = "@" + ",".join(restraint_atom_names) elif restraint == "heavy": - restraint_mask = "!:WAT & !@H=" + restraint_mask = "!:WAT & !@%NA,CL & !@H=" elif restraint == "all": - restraint_mask = "!:WAT" + restraint_mask = "!:WAT & !@%NA,CL" # We can't do anything about a custom restraint, since we don't # know anything about the atoms. @@ -304,13 +310,22 @@ def generateAmberConfig(self, extra_options=None, extra_lines=None): "AMBER atom 'restraintmask' exceeds 256 character limit!" ) - protocol_dict["ntr"] = 1 - force_constant = self.protocol.getForceConstant()._sire_unit - force_constant = force_constant.to( - _SireUnits.kcal_per_mol / _SireUnits.angstrom2 - ) - protocol_dict["restraint_wt"] = force_constant - protocol_dict["restraintmask"] = f'"{restraint_mask}"' + else: + restraint_mask = None + + if restraint_mask or alchemical_ion_mask: + if restraint_mask and alchemical_ion_mask: + restraint_mask = f"{restraint_mask} | {alchemical_ion_mask}" + elif alchemical_ion_mask: + restraint_mask = alchemical_ion_mask + + protocol_dict["ntr"] = 1 + force_constant = self.protocol.getForceConstant()._sire_unit + force_constant = force_constant.to( + _SireUnits.kcal_per_mol / _SireUnits.angstrom2 + ) + protocol_dict["restraint_wt"] = force_constant + protocol_dict["restraintmask"] = f'"{restraint_mask}"' # Pressure control. if not isinstance(self.protocol, _Protocol.Minimisation): @@ -318,9 +333,9 @@ def generateAmberConfig(self, extra_options=None, extra_lines=None): # Don't use barostat for vacuum simulations. if self._has_box and self._has_water: protocol_dict["ntp"] = 1 # Isotropic pressure scaling. - protocol_dict[ - "pres0" - ] = f"{self.protocol.getPressure().bar().value():.5f}" # Pressure in bar. + protocol_dict["pres0"] = ( + f"{self.protocol.getPressure().bar().value():.5f}" # Pressure in bar. + ) if isinstance(self.protocol, _Protocol.Equilibration): protocol_dict["barostat"] = 1 # Berendsen barostat. else: @@ -466,23 +481,23 @@ def generateGromacsConfig( protocol_dict["cutoff-scheme"] = "Verlet" # Use Verlet pair lists. if self._has_box and self._has_water: protocol_dict["ns-type"] = "grid" # Use a grid to search for neighbours. - protocol_dict[ - "nstlist" - ] = "20" # Rebuild neighbour list every 20 steps. Recommended in the manual for parallel simulations and/or non-bonded force calculation on the GPU. + protocol_dict["nstlist"] = ( + "20" # Rebuild neighbour list every 20 steps. Recommended in the manual for parallel simulations and/or non-bonded force calculation on the GPU. + ) protocol_dict["rlist"] = "0.8" # Set short-range cutoff. protocol_dict["rvdw"] = "0.8" # Set van der Waals cutoff. protocol_dict["rcoulomb"] = "0.8" # Set Coulomb cutoff. protocol_dict["coulombtype"] = "PME" # Fast smooth Particle-Mesh Ewald. - protocol_dict[ - "DispCorr" - ] = "EnerPres" # Dispersion corrections for energy and pressure. + protocol_dict["DispCorr"] = ( + "EnerPres" # Dispersion corrections for energy and pressure. + ) else: # Perform vacuum simulations by implementing pseudo-PBC conditions, # i.e. run calculation in a near-infinite box (333.3 nm). # c.f.: https://pubmed.ncbi.nlm.nih.gov/29678588 - protocol_dict[ - "nstlist" - ] = "1" # Single neighbour list (all particles interact). + protocol_dict["nstlist"] = ( + "1" # Single neighbour list (all particles interact). + ) protocol_dict["rlist"] = "333.3" # "Infinite" short-range cutoff. protocol_dict["rvdw"] = "333.3" # "Infinite" van der Waals cutoff. protocol_dict["rcoulomb"] = "333.3" # "Infinite" Coulomb cutoff. @@ -503,12 +518,12 @@ def generateGromacsConfig( # 4ps time constant for pressure coupling. # As the tau-p has to be 10 times larger than nstpcouple * dt (4 fs) protocol_dict["tau-p"] = 4 - protocol_dict[ - "ref-p" - ] = f"{self.protocol.getPressure().bar().value():.5f}" # Pressure in bar. - protocol_dict[ - "compressibility" - ] = "4.5e-5" # Compressibility of water. + protocol_dict["ref-p"] = ( + f"{self.protocol.getPressure().bar().value():.5f}" # Pressure in bar. + ) + protocol_dict["compressibility"] = ( + "4.5e-5" # Compressibility of water. + ) else: _warnings.warn( "Cannot use a barostat for a vacuum or non-periodic simulation" @@ -521,9 +536,9 @@ def generateGromacsConfig( else: protocol_dict["integrator"] = "md" # leap-frog dynamics. protocol_dict["tcoupl"] = "v-rescale" - protocol_dict[ - "tc-grps" - ] = "system" # A single temperature group for the entire system. + protocol_dict["tc-grps"] = ( + "system" # A single temperature group for the entire system. + ) protocol_dict["tau-t"] = "{:.5f}".format( self.protocol.getTauT().picoseconds().value() ) # Collision frequency (ps). @@ -538,12 +553,12 @@ def generateGromacsConfig( timestep = self.protocol.getTimeStep().picoseconds().value() end_time = _math.floor(timestep * self._steps) - protocol_dict[ - "annealing" - ] = "single" # Single sequence of annealing points. - protocol_dict[ - "annealing-npoints" - ] = 2 # Two annealing points for "system" temperature group. + protocol_dict["annealing"] = ( + "single" # Single sequence of annealing points. + ) + protocol_dict["annealing-npoints"] = ( + 2 # Two annealing points for "system" temperature group. + ) # Linearly change temperature between start and end times. protocol_dict["annealing-time"] = "0 %d" % end_time @@ -613,20 +628,20 @@ def tranform(charge, LJ): "temperature", ]: if name in LambdaValues: - protocol_dict[ - "{:<20}".format("{}-lambdas".format(name)) - ] = " ".join( - list(map("{:.5f}".format, LambdaValues[name].to_list())) + protocol_dict["{:<20}".format("{}-lambdas".format(name))] = ( + " ".join( + list(map("{:.5f}".format, LambdaValues[name].to_list())) + ) ) - protocol_dict[ - "init-lambda-state" - ] = self.protocol.getLambdaIndex() # Current lambda value. - protocol_dict[ - "nstcalcenergy" - ] = self._report_interval # Calculate energies every report_interval steps. - protocol_dict[ - "nstdhdl" - ] = self._report_interval # Write gradients every report_interval steps. + protocol_dict["init-lambda-state"] = ( + self.protocol.getLambdaIndex() + ) # Current lambda value. + protocol_dict["nstcalcenergy"] = ( + self._report_interval + ) # Calculate energies every report_interval steps. + protocol_dict["nstdhdl"] = ( + self._report_interval + ) # Write gradients every report_interval steps. # Handle the combination of multiple distance restraints and perturbation type # of "release_restraint". In this case, the force constant of the "permanent" @@ -833,18 +848,18 @@ def generateSomdConfig( # Free energies. if isinstance(self.protocol, _Protocol._FreeEnergyMixin): if not isinstance(self.protocol, _Protocol.Minimisation): - protocol_dict[ - "constraint" - ] = "hbonds-notperturbed" # Handle hydrogen perturbations. - protocol_dict[ - "energy frequency" - ] = 250 # Write gradients every 250 steps. + protocol_dict["constraint"] = ( + "hbonds-notperturbed" # Handle hydrogen perturbations. + ) + protocol_dict["energy frequency"] = ( + 250 # Write gradients every 250 steps. + ) protocol = [str(x) for x in self.protocol.getLambdaValues()] protocol_dict["lambda array"] = ", ".join(protocol) - protocol_dict[ - "lambda_val" - ] = self.protocol.getLambda() # Current lambda value. + protocol_dict["lambda_val"] = ( + self.protocol.getLambda() + ) # Current lambda value. try: # RBFE res_num = ( @@ -861,9 +876,9 @@ def generateSomdConfig( .value() ) - protocol_dict[ - "perturbed residue number" - ] = res_num # Perturbed residue number. + protocol_dict["perturbed residue number"] = ( + res_num # Perturbed residue number. + ) # Put everything together in a line-by-line format. total_dict = {**protocol_dict, **extra_options} diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py index 7bebe86ee..7c25c30f7 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py @@ -32,20 +32,22 @@ from math import isclose as _isclose from warnings import warn as _warn -from sire.legacy import Base as _SireBase -from sire.legacy import IO as _SireIO -from sire.legacy import MM as _SireMM -from sire.legacy import Maths as _SireMaths -from sire.legacy import Mol as _SireMol -from sire.legacy import System as _SireSystem -from sire.legacy import Units as _SireUnits +import numpy as _np +from sire.legacy import ( + Base as _SireBase, + IO as _SireIO, + MM as _SireMM, + Maths as _SireMaths, + Mol as _SireMol, + System as _SireSystem, + Units as _SireUnits, +) +from ._sire_wrapper import SireWrapper as _SireWrapper from .. import _isVerbose +from ..Types import Coordinate as _Coordinate, Length as _Length, Vector as _BSSVector +from ..Units.Length import angstrom as _angstrom from .._Exceptions import IncompatibleError as _IncompatibleError -from ..Types import Coordinate as _Coordinate -from ..Types import Length as _Length - -from ._sire_wrapper import SireWrapper as _SireWrapper class Molecule(_SireWrapper): @@ -1879,6 +1881,20 @@ def _getPerturbationIndices(self): return idxs + def getCOMIdx(self): + """Get the index of the atom that closest to the center of mass.""" + if self.isPerturbable(): + property_map = {"coordinates": "coordinates0", "mass": "mass0"} + else: + property_map = {"coordinates": "coordinates", "mass": "mass"} + coords = self.coordinates(property_map=property_map) + com = self._getCenterOfMass(property_map=property_map) + com = _BSSVector(*[e / _angstrom for e in com]) + diffs = [coord.toVector() - com for coord in coords] + sq_distances = [diff.dot(diff) for diff in diffs] + idx = int(_np.argmin(sq_distances)) + return idx + # Import at bottom of module to avoid circular dependency. from ._atom import Atom as _Atom diff --git a/tests/Sandpit/Exscientia/Align/test_alchemical_ion.py b/tests/Sandpit/Exscientia/Align/test_alchemical_ion.py index fa2a0aeb6..5caee6425 100644 --- a/tests/Sandpit/Exscientia/Align/test_alchemical_ion.py +++ b/tests/Sandpit/Exscientia/Align/test_alchemical_ion.py @@ -1,10 +1,12 @@ import pytest import BioSimSpace.Sandpit.Exscientia as BSS -from BioSimSpace.Sandpit.Exscientia.Align._alch_ion import _mark_alchemical_ion +from BioSimSpace.Sandpit.Exscientia.Align._alch_ion import ( + _get_protein_com_idx, + _mark_alchemical_ion, +) from BioSimSpace.Sandpit.Exscientia._SireWrappers import Molecule - -from tests.conftest import root_fp, has_gromacs +from tests.conftest import has_gromacs, root_fp @pytest.fixture @@ -50,3 +52,8 @@ def test_getAlchemicalIon(input_system, isalchem, request): def test_getAlchemicalIonIdx(alchemical_ion_system): index = alchemical_ion_system.getAlchemicalIonIdx() assert index == 680 + + +def test_get_protein_com_idx(alchemical_ion_system): + index = _get_protein_com_idx(alchemical_ion_system) + assert index == 8 diff --git a/tests/Sandpit/Exscientia/Process/test_amber.py b/tests/Sandpit/Exscientia/Process/test_amber.py index 73866d01a..1a6daded5 100644 --- a/tests/Sandpit/Exscientia/Process/test_amber.py +++ b/tests/Sandpit/Exscientia/Process/test_amber.py @@ -5,11 +5,9 @@ import numpy as np import pandas as pd import pytest -import shutil import BioSimSpace.Sandpit.Exscientia as BSS - -from tests.Sandpit.Exscientia.conftest import url, has_amber, has_pyarrow +from tests.Sandpit.Exscientia.conftest import has_amber, has_pyarrow from tests.conftest import root_fp diff --git a/tests/Sandpit/Exscientia/Process/test_gromacs.py b/tests/Sandpit/Exscientia/Process/test_gromacs.py index 09c3e3564..9a1886e97 100644 --- a/tests/Sandpit/Exscientia/Process/test_gromacs.py +++ b/tests/Sandpit/Exscientia/Process/test_gromacs.py @@ -1,13 +1,12 @@ import math -import numpy as np -import pytest -import pandas as pd import shutil - from pathlib import Path -import BioSimSpace.Sandpit.Exscientia as BSS +import numpy as np +import pandas as pd +import pytest +import BioSimSpace.Sandpit.Exscientia as BSS from BioSimSpace.Sandpit.Exscientia.Align import decouple from BioSimSpace.Sandpit.Exscientia.FreeEnergy import Restraint from BioSimSpace.Sandpit.Exscientia.Units.Angle import radian @@ -17,15 +16,13 @@ from BioSimSpace.Sandpit.Exscientia.Units.Temperature import kelvin from BioSimSpace.Sandpit.Exscientia.Units.Time import picosecond from BioSimSpace.Sandpit.Exscientia.Units.Volume import nanometer3 - from tests.Sandpit.Exscientia.conftest import ( - url, - has_alchemlyb, has_alchemtest, has_amber, has_gromacs, has_openff, has_pyarrow, + url, ) from tests.conftest import root_fp diff --git a/tests/Sandpit/Exscientia/Process/test_position_restraint.py b/tests/Sandpit/Exscientia/Process/test_position_restraint.py index 7355adc70..7c16427c1 100644 --- a/tests/Sandpit/Exscientia/Process/test_position_restraint.py +++ b/tests/Sandpit/Exscientia/Process/test_position_restraint.py @@ -1,17 +1,20 @@ -from difflib import unified_diff - import itertools import os +from difflib import unified_diff import pandas as pd import pytest +import sire as sr +from sire.legacy import Units as SireUnits from sire.legacy.IO import AmberRst import BioSimSpace.Sandpit.Exscientia as BSS +from BioSimSpace.Sandpit.Exscientia.Align._alch_ion import _mark_alchemical_ion from BioSimSpace.Sandpit.Exscientia.Units.Energy import kj_per_mol from BioSimSpace.Sandpit.Exscientia.Units.Length import angstrom - +from BioSimSpace.Sandpit.Exscientia._SireWrappers import Molecule from tests.Sandpit.Exscientia.conftest import has_amber, has_gromacs, has_openff +from tests.conftest import root_fp @pytest.fixture @@ -22,6 +25,41 @@ def system(): return BSS.Align.merge(mol0, mol1).toSystem() +@pytest.fixture(scope="session") +def alchemical_ion_system(): + """A large protein system for re-use.""" + system = BSS.IO.readMolecules( + [f"{root_fp}/input/ala.top", f"{root_fp}/input/ala.crd"] + ) + solvated = BSS.Solvent.tip3p( + system, box=[4 * BSS.Units.Length.nanometer] * 3, ion_conc=0.15 + ) + ion = solvated.getMolecule(-1) + pert_ion = BSS.Align.merge(ion, ion, mapping={0: 0}) + pert_ion._sire_object = ( + pert_ion.getAtoms()[0] + ._sire_object.edit() + .setProperty("charge1", 0 * SireUnits.mod_electron) + .molecule() + ) + alchemcial_ion = _mark_alchemical_ion(pert_ion) + solvated.updateMolecule(solvated.getIndex(ion), alchemcial_ion) + return solvated + + +@pytest.fixture(scope="session") +def alchemical_ion_system_psores(alchemical_ion_system): + # Create a reference system with different coordinate + system = alchemical_ion_system.copy() + mol = system.getMolecule(0) + sire_mol = mol._sire_object + atoms = sire_mol.cursor().atoms() + atoms[0]["coordinates"] = sr.maths.Vector(0, 0, 0) + new_mol = atoms.commit() + system.updateMolecule(0, Molecule(new_mol)) + return system + + @pytest.fixture def ref_system(system): mol = system[0] @@ -146,3 +184,86 @@ def test_amber(protocol, system, ref_system, tmp_path): # We are pointing the reference to the correct file assert f"{proc._work_dir}/{proc.getArgs()['-ref']}" == proc._ref_file + + +@pytest.mark.parametrize( + "restraint", + ["backbone", "heavy", "all", "none"], +) +def test_gromacs(alchemical_ion_system, restraint, alchemical_ion_system_psores): + protocol = BSS.Protocol.FreeEnergy(restraint=restraint) + process = BSS.Process.Gromacs( + alchemical_ion_system, + protocol, + name="test", + reference_system=alchemical_ion_system_psores, + ) + + # Test the position restraint for protein center + with open(f"{process.workDir()}/posre_0001.itp", "r") as f: + posres = f.read().split("\n") + posres = [tuple(line.split()) for line in posres] + + assert ("9", "1", "4184.0", "4184.0", "4184.0") in posres + + # Test the position restraint for alchemical ion + with open(f"{process.workDir()}/test.top", "r") as f: + top = f.read() + lines = top[top.index("Merged_Molecule") :].split("\n") + assert lines[6] == '#include "posre_0002.itp"' + + with open(f"{process.workDir()}/posre_0002.itp", "r") as f: + posres = f.read().split("\n") + + assert posres[2].split() == ["1", "1", "4184.0", "4184.0", "4184.0"] + + # Test if the original coordinate is correct + with open(f"{process.workDir()}/test.gro", "r") as f: + gro = f.read().splitlines() + assert gro[2].split() == ["1ACE", "HH31", "1", "1.791", "1.610", "2.058"] + + # Test if the reference coordinate is passed + with open(f"{process.workDir()}/test_ref.gro", "r") as f: + gro = f.read().splitlines() + assert gro[2].split() == ["1ACE", "HH31", "1", "0.000", "0.000", "0.000"] + + +@pytest.mark.parametrize( + ("restraint", "target"), + [ + ("backbone", "@5-7,9,15-17 | @2148 | @8"), + ("heavy", "@2,5-7,9,11,15-17,19 | @2148 | @8"), + ("all", "@1-22 | @2148 | @8"), + ("none", "@2148 | @8"), + ], +) +def test_amber(alchemical_ion_system, restraint, target, alchemical_ion_system_psores): + # Create an equilibration protocol with backbone restraints. + protocol = BSS.Protocol.Equilibration(restraint=restraint) + + # Create the process object. + process = BSS.Process.Amber( + alchemical_ion_system, + protocol, + name="test", + reference_system=alchemical_ion_system_psores, + ) + + # Check that the correct restraint mask is in the config. + config = " ".join(process.getConfig()) + assert target in config + # Check is the reference file is passed to the cmd + assert "-ref test_ref.rst7" in process.getArgString() + + # Test if the original coordinate is correct + original = BSS.IO.readMolecules( + [f"{process.workDir()}/test.prm7", f"{process.workDir()}/test.rst7"] + ) + original_crd = original.getMolecule(0).coordinates()[0] + assert str(original_crd) == "(17.9138 A, 16.0981 A, 20.5786 A)" + # Test if the reference coordinate is passed + ref = BSS.IO.readMolecules( + [f"{process.workDir()}/test.prm7", f"{process.workDir()}/test_ref.rst7"] + ) + ref_crd = ref.getMolecule(0).coordinates()[0] + assert str(ref_crd) == "(0.0000e+00 A, 0.0000e+00 A, 0.0000e+00 A)" From 43999051b28c2d064269f0cb6a0672cc36d6ccda Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 13 Mar 2024 13:57:49 +0000 Subject: [PATCH 049/121] Don't use pmemd.cuda for vacuum FEP simulations. --- python/BioSimSpace/MD/_md.py | 12 ++++++- python/BioSimSpace/Process/_amber.py | 39 +++++++++++++++++++---- python/BioSimSpace/_Config/_amber.py | 8 +++-- python/BioSimSpace/_Config/_config.py | 44 ++++++++++++++++++++++---- python/BioSimSpace/_Config/_gromacs.py | 8 +++-- python/BioSimSpace/_Config/_somd.py | 8 +++-- 6 files changed, 99 insertions(+), 20 deletions(-) diff --git a/python/BioSimSpace/MD/_md.py b/python/BioSimSpace/MD/_md.py index e5d3e510c..d70e8d7ab 100644 --- a/python/BioSimSpace/MD/_md.py +++ b/python/BioSimSpace/MD/_md.py @@ -176,10 +176,20 @@ def _find_md_engines(system, protocol, engine="AUTO", gpu_support=False): # Special handling for AMBER which has a custom executable finding # function. if engine == "AMBER": + from .._Config import Amber as _AmberConfig from ..Process._amber import _find_exe + # Is this a vacuum simulation. + is_vacuum = not ( + _AmberConfig.hasBox(system) or _AmberConfig.hasWater(system) + ) + try: - exe = _find_exe(is_gpu=gpu_support, is_free_energy=is_free_energy) + exe = _find_exe( + is_gpu=gpu_support, + is_free_energy=is_free_energy, + is_vacuum=is_vacuum, + ) found_engines.append(engine) found_exes.append(exe) except: diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index 4bf4a5f1c..8cf36cc39 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -168,7 +168,15 @@ def __init__( else: is_free_energy = False - self._exe = _find_exe(is_gpu=is_gpu, is_free_energy=is_free_energy) + # Check whether this is a vacuum simulation. + is_vacuum = not ( + _AmberConfig.hasBox(self._system, self._property_map) + or _AmberConfig.hasWater(self._system) + ) + + self._exe = _find_exe( + is_gpu=is_gpu, is_free_energy=is_free_energy, is_vacuum=is_vacuum + ) else: # Make sure executable exists. if _os.path.isfile(exe): @@ -2718,7 +2726,7 @@ def _get_stdout_record( return None -def _find_exe(is_gpu=False, is_free_energy=False): +def _find_exe(is_gpu=False, is_free_energy=False, is_vacuum=False): """ Helper function to search for an AMBER executable. @@ -2729,7 +2737,10 @@ def _find_exe(is_gpu=False, is_free_energy=False): Whether to search for a GPU-enabled executable. is_free_energy : bool - Whether the executable is for a free energy protocol. + Whether this is a free energy simulation. + + is_vacuum : bool + Whether this is a vacuum simulation. Returns ------- @@ -2744,13 +2755,27 @@ def _find_exe(is_gpu=False, is_free_energy=False): if not isinstance(is_free_energy, bool): raise TypeError("'is_free_energy' must be of type 'bool'.") - # If the user has requested a GPU-enabled executable, search for pmemd.cuda only. + if not isinstance(is_vacuum, bool): + raise TypeError("'is_vacuum' must be of type 'bool'.") + + # It is not possible to use implicit solvent for free energy simulations + # on GPU, so we fall back to pmemd for vacuum free energy simulations. + + if is_gpu and is_free_energy and is_vacuum: + _warnings.warn( + "Implicit solvent is not supported for free energy simulations on GPU. " + "Falling back to pmemd for vacuum free energy simulations." + ) + is_gpu = False + if is_gpu: targets = ["pmemd.cuda"] else: - # If the this is a free energy simulation, then only use pmemd or pmemd.cuda. - if is_free_energy: - targets = ["pmemd"] + if is_free_energy and not is_vacuum: + if is_vacuum: + targets = ["pmemd"] + else: + targets = ["pmemd", "pmemd.cuda"] else: targets = ["pmemd", "sander"] diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index d8de5295f..4fb9d4761 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -188,7 +188,9 @@ def createConfig( protocol_dict["ntf"] = 2 # Periodic boundary conditions. - if not self.hasBox() or not self.hasWater(): + if not self.hasBox(self._system, self._property_map) or not self.hasWater( + self._system + ): # No periodic box. protocol_dict["ntb"] = 0 # Non-bonded cut-off. @@ -277,7 +279,9 @@ def createConfig( if not isinstance(self._protocol, _Protocol.Minimisation): if self._protocol.getPressure() is not None: # Don't use barostat for vacuum simulations. - if self.hasBox() and self.hasWater(): + if self.hasBox(self._system, self._property_map) and self.hasWater( + self._system + ): # Isotropic pressure scaling. protocol_dict["ntp"] = 1 # Pressure in bar. diff --git a/python/BioSimSpace/_Config/_config.py b/python/BioSimSpace/_Config/_config.py index 3aea03484..9005dbb78 100644 --- a/python/BioSimSpace/_Config/_config.py +++ b/python/BioSimSpace/_Config/_config.py @@ -79,22 +79,43 @@ def __init__(self, system, protocol, property_map={}): self._protocol = protocol self._property_map = property_map - def hasBox(self): + @staticmethod + def hasBox(system, property_map={}): """ Whether the system has a box. + Parameters + ---------- + + system : :class:`System ` + The molecular system. + + property_map : dict + A dictionary that maps system "properties" to their user defined + values. This allows the user to refer to properties with their + own naming scheme, e.g. { "charge" : "my-charge" } + Returns ------- has_box : bool Whether the system has a simulation box. """ - space_prop = self._property_map.get("space", "space") - if space_prop in self._system._sire_object.propertyKeys(): + + if not isinstance(system, _System): + raise TypeError( + "'system' must be of type 'BioSimSpace._SireWrappers.System'" + ) + + if not isinstance(property_map, dict): + raise TypeError("'property_map' must be of type 'dict'") + + space_prop = property_map.get("space", "space") + if space_prop in system._sire_object.propertyKeys(): try: # Make sure that we have a periodic box. The system will now have # a default cartesian space. - box = self._system._sire_object.property(space_prop) + box = system._sire_object.property(space_prop) has_box = box.isPeriodic() except: has_box = False @@ -104,17 +125,28 @@ def hasBox(self): return has_box - def hasWater(self): + @staticmethod + def hasWater(system): """ Whether the system is contains water molecules. + Parameters + + system : :class:`System ` + The molecular system. + Returns ------- has_water : bool Whether the system contains water molecules. """ - return self._system.nWaterMolecules() > 0 + if not isinstance(system, _System): + raise TypeError( + "'system' must be of type 'BioSimSpace._SireWrappers.System'" + ) + + return system.nWaterMolecules() > 0 def reportInterval(self): """ diff --git a/python/BioSimSpace/_Config/_gromacs.py b/python/BioSimSpace/_Config/_gromacs.py index 23c50e613..9be324be2 100644 --- a/python/BioSimSpace/_Config/_gromacs.py +++ b/python/BioSimSpace/_Config/_gromacs.py @@ -145,7 +145,9 @@ def createConfig(self, version=None, extra_options={}, extra_lines=[]): protocol_dict["pbc"] = "xyz" # Use Verlet pair lists. protocol_dict["cutoff-scheme"] = "Verlet" - if self.hasBox() and self.hasWater(): + if self.hasBox(self._system, self._property_map) and self.hasWater( + self._system + ): # Use a grid to search for neighbours. protocol_dict["ns-type"] = "grid" # Rebuild neighbour list every 20 steps. @@ -186,7 +188,9 @@ def createConfig(self, version=None, extra_options={}, extra_lines=[]): if not isinstance(self._protocol, _Protocol.Minimisation): if self._protocol.getPressure() is not None: # Don't use barostat for vacuum simulations. - if self.hasBox() and self.hasWater(): + if self.hasBox(self._system, self._property_map) and self.hasWater( + self._system + ): # Barostat type. if version and version >= 2021: protocol_dict["pcoupl"] = "c-rescale" diff --git a/python/BioSimSpace/_Config/_somd.py b/python/BioSimSpace/_Config/_somd.py index 12ce38b7d..7ee18fa7a 100644 --- a/python/BioSimSpace/_Config/_somd.py +++ b/python/BioSimSpace/_Config/_somd.py @@ -176,7 +176,9 @@ def createConfig(self, extra_options={}, extra_lines=[]): if self.hasWater(): # Solvated box. protocol_dict["reaction field dielectric"] = "78.3" - if not self.hasBox() or not self.hasWater(): + if not self.hasBox(self._system, self._property_map) or not self.hasWater( + self._system + ): # No periodic box. protocol_dict["cutoff type"] = "cutoffnonperiodic" else: @@ -199,7 +201,9 @@ def createConfig(self, extra_options={}, extra_lines=[]): if not isinstance(self._protocol, _Protocol.Minimisation): if self._protocol.getPressure() is not None: # Don't use barostat for vacuum simulations. - if self.hasBox() and self.hasWater(): + if self.hasBox(self._system, self._property_map) and self.hasWater( + self._system + ): # Enable barostat. protocol_dict["barostat"] = True pressure = self._protocol.getPressure().atm().value() From 777d4590846de474b0dfc8219e5b2770ce248438 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 14 Mar 2024 12:30:32 +0000 Subject: [PATCH 050/121] Add AMBER FEP analysis test. --- tests/FreeEnergy/test_relative.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/FreeEnergy/test_relative.py b/tests/FreeEnergy/test_relative.py index f93f49e61..6d01fe81f 100644 --- a/tests/FreeEnergy/test_relative.py +++ b/tests/FreeEnergy/test_relative.py @@ -54,8 +54,9 @@ def expected_results(): """A dictionary of expected FEP results.""" return { - "somd": {"mbar": -6.3519, "ti": -6.3209}, + "amber": {"mbar": -12.5939, "ti": -13.6850}, "gromacs": {"mbar": -6.0238, "ti": -8.4158}, + "somd": {"mbar": -6.3519, "ti": -6.3209}, } @@ -73,7 +74,7 @@ def test_setup_gromacs(perturbable_system): @pytest.mark.skipif( has_alchemlyb is False, reason="Requires alchemlyb to be installed." ) -@pytest.mark.parametrize("engine", ["somd", "gromacs"]) +@pytest.mark.parametrize("engine", ["amber", "gromacs", "somd"]) @pytest.mark.parametrize("estimator", ["mbar", "ti"]) def test_analysis(fep_output, engine, estimator, expected_results): """Test that the free energy analysis works as expected.""" From 31b34df77762975720672994ddd047729d824fe0 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 14 Mar 2024 12:34:36 +0000 Subject: [PATCH 051/121] Add SOMD1 compatibility mode as an option for AMBER/GROMACS FEP. --- python/BioSimSpace/Process/_amber.py | 20 +- python/BioSimSpace/Process/_gromacs.py | 11 +- python/BioSimSpace/Process/_somd.py | 501 +++++++++++++++++++++++++ 3 files changed, 529 insertions(+), 3 deletions(-) diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index 8cf36cc39..901319a8a 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -239,9 +239,9 @@ def __init__( self._input_files.append(self._ref_file) # Now set up the working directory for the process. - self._setup() + self._setup(**kwargs) - def _setup(self): + def _setup(self, **kwargs): """Setup the input files and working directory ready for simulation.""" # Create the input files... @@ -254,6 +254,22 @@ def _setup(self): # Create the squashed system. if isinstance(self._protocol, _FreeEnergyMixin): + # Check that the system contains a perturbable molecule. + if self._system.nPerturbableMolecules() == 0: + raise ValueError( + "'BioSimSpace.Protocol.FreeEnergy' requires a " + "perturbable molecule!" + ) + + # Apply SOMD1 compatibility to the perturbation. + if ( + "somd1_compatibility" in kwargs + and kwargs.get("somd1_compatibility") is True + ): + from ._somd import _somd1_compatibility + + system = _somd1_compatibility(system) + system, self._mapping = _squash( system, explicit_dummies=self._explicit_dummies ) diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index c4e6e36a9..21e661a12 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -241,7 +241,7 @@ def __init__( ) # Now set up the working directory for the process. - self._setup() + self._setup(**kwargs) def _setup(self): """Setup the input files and working directory ready for simulation.""" @@ -268,6 +268,15 @@ def _setup(self): ) raise NotImplementedError(msg) + # Apply SOMD1 compatibility to the perturbation. + if ( + "somd1_compatibility" in kwargs + and kwargs.get("somd1_compatibility") is True + ): + from ._somd import _somd1_compatibility + + system = _somd1_compatibility(system) + else: # Check for perturbable molecules and convert to the chosen end state. system = self._checkPerturbable(system) diff --git a/python/BioSimSpace/Process/_somd.py b/python/BioSimSpace/Process/_somd.py index c64f8e689..1f3094daf 100644 --- a/python/BioSimSpace/Process/_somd.py +++ b/python/BioSimSpace/Process/_somd.py @@ -3047,3 +3047,504 @@ def _random_suffix(basename, size=4, chars=_string.ascii_uppercase + _string.dig + "AMBER atom names can only be 4 characters wide." ) return "".join(_random.choice(chars) for _ in range(size - basename_size)) + + +def _somd1_compatibility(system): + """ + Makes a perturbation SOMD1 compatible. + + Parameters + ---------- + + system : :class:`System ` + The system containing the molecules to be perturbed. + + Returns + ------- + + system : :class:`System ` + The updated system. + """ + + # Check the system is a Sire system. + if not isinstance(system, _System): + raise TypeError("'system' must of type 'BioSimSpace._SireWrappers.System'") + + # Search for perturbable molecules. + pert_mols = system.getPerturbableMolecules() + if len(pert_mols) == 0: + raise KeyError("No perturbable molecules in the system") + + # Store a dummy element. + dummy = _SireMol.Element("Xx") + + for mol in pert_mols: + # Get the underlying Sire molecule. + mol = mol._sire_object + + # Store the molecule info. + info = mol.info() + + # Get an editable version of the molecule. + edit_mol = mol.edit() + + ########################## + # First process the bonds. + ########################## + + new_bonds0 = _SireMM.TwoAtomFunctions(mol.info()) + new_bonds1 = _SireMM.TwoAtomFunctions(mol.info()) + + # Extract the bonds at lambda = 0 and 1. + bonds0 = mol.property("bond0").potentials() + bonds1 = mol.property("bond1").potentials() + + # Dictionaries to store the BondIDs at lambda = 0 and 1. + bonds0_idx = {} + bonds1_idx = {} + + # Loop over all bonds at lambda = 0. + for idx, bond in enumerate(bonds0): + # Get the AtomIdx for the atoms in the bond. + idx0 = info.atom_idx(bond.atom0()) + idx1 = info.atom_idx(bond.atom1()) + + # Create the BondID. + bond_id = _SireMol.BondID(idx0, idx1) + + # Add to the list of ids. + bonds0_idx[bond_id] = idx + + # Loop over all bonds at lambda = 1. + for idx, bond in enumerate(bonds1): + # Get the AtomIdx for the atoms in the bond. + idx0 = info.atom_idx(bond.atom0()) + idx1 = info.atom_idx(bond.atom1()) + + # Create the BondID. + bond_id = _SireMol.BondID(idx0, idx1) + + # Add to the list of ids. + if bond_id.mirror() in bonds0_idx: + bonds1_idx[bond_id.mirror()] = idx + else: + bonds1_idx[bond_id] = idx + + # Now work out the BondIDs that are unique at lambda = 0 and 1 + # as well as those that are shared. + bonds0_unique_idx = {} + bonds1_unique_idx = {} + bonds_shared_idx = {} + + # lambda = 0. + for idx in bonds0_idx.keys(): + if idx not in bonds1_idx.keys(): + bonds0_unique_idx[idx] = bonds0_idx[idx] + else: + bonds_shared_idx[idx] = (bonds0_idx[idx], bonds1_idx[idx]) + + # lambda = 1. + for idx in bonds1_idx.keys(): + if idx not in bonds0_idx.keys(): + bonds1_unique_idx[idx] = bonds1_idx[idx] + elif idx not in bonds_shared_idx.keys(): + bonds_shared_idx[idx] = (bonds0_idx[idx], bonds1_idx[idx]) + + # Loop over the shared bonds. + for idx0, idx1 in bonds_shared_idx.values(): + # Get the bond potentials. + p0 = bonds0[idx0] + p1 = bonds1[idx1] + + # Get the AtomIdx for the atoms in the angle. + idx0 = p0.atom0() + idx1 = p0.atom1() + + # Check whether a dummy atoms are present in the lambda = 0 + # and lambda = 1 states. + initial_dummy = _has_dummy(mol, [idx0, idx1]) + final_dummy = _has_dummy(mol, [idx0, idx1], True) + + # If there is a dummy, then set the potential to the opposite state. + # This should already be the case, but we explicitly set it here. + + if initial_dummy: + new_bonds0.set(idx0, idx1, p1.function()) + new_bonds1.set(idx0, idx1, p1.function()) + elif final_dummy: + new_bonds0.set(idx0, idx1, p0.function()) + new_bonds1.set(idx0, idx1, p0.function()) + else: + new_bonds0.set(idx0, idx1, p0.function()) + new_bonds1.set(idx0, idx1, p1.function()) + + # Set the new bonded terms. + edit_mol = edit_mol.set_property("bond0", new_bonds0).molecule() + edit_mol = edit_mol.set_property("bond1", new_bonds1).molecule() + + ######################### + # Now process the angles. + ######################### + + new_angles0 = _SireMM.ThreeAtomFunctions(mol.info()) + new_angles1 = _SireMM.ThreeAtomFunctions(mol.info()) + + # Extract the angles at lambda = 0 and 1. + angles0 = mol.property("angle0").potentials() + angles1 = mol.property("angle1").potentials() + + # Dictionaries to store the AngleIDs at lambda = 0 and 1. + angles0_idx = {} + angles1_idx = {} + + # Loop over all angles at lambda = 0. + for idx, angle in enumerate(angles0): + # Get the AtomIdx for the atoms in the angle. + idx0 = info.atom_idx(angle.atom0()) + idx1 = info.atom_idx(angle.atom1()) + idx2 = info.atom_idx(angle.atom2()) + + # Create the AngleID. + angle_id = _SireMol.AngleID(idx0, idx1, idx2) + + # Add to the list of ids. + angles0_idx[angle_id] = idx + + # Loop over all angles at lambda = 1. + for idx, angle in enumerate(angles1): + # Get the AtomIdx for the atoms in the angle. + idx0 = info.atom_idx(angle.atom0()) + idx1 = info.atom_idx(angle.atom1()) + idx2 = info.atom_idx(angle.atom2()) + + # Create the AngleID. + angle_id = _SireMol.AngleID(idx0, idx1, idx2) + + # Add to the list of ids. + if angle_id.mirror() in angles0_idx: + angles1_idx[angle_id.mirror()] = idx + else: + angles1_idx[angle_id] = idx + + # Now work out the AngleIDs that are unique at lambda = 0 and 1 + # as well as those that are shared. + angles0_unique_idx = {} + angles1_unique_idx = {} + angles_shared_idx = {} + + # lambda = 0. + for idx in angles0_idx.keys(): + if idx not in angles1_idx.keys(): + angles0_unique_idx[idx] = angles0_idx[idx] + else: + angles_shared_idx[idx] = (angles0_idx[idx], angles1_idx[idx]) + + # lambda = 1. + for idx in angles1_idx.keys(): + if idx not in angles0_idx.keys(): + angles1_unique_idx[idx] = angles1_idx[idx] + elif idx not in angles_shared_idx.keys(): + angles_shared_idx[idx] = (angles0_idx[idx], angles1_idx[idx]) + + # Loop over the angles. + for idx0, idx1 in angles_shared_idx.values(): + # Get the angle potentials. + p0 = angles0[idx0] + p1 = angles1[idx1] + + # Get the AtomIdx for the atoms in the angle. + idx0 = p0.atom0() + idx1 = p0.atom1() + idx2 = p0.atom2() + + # Check whether a dummy atoms are present in the lambda = 0 + # and lambda = 1 states. + initial_dummy = _has_dummy(mol, [idx0, idx1, idx2]) + final_dummy = _has_dummy(mol, [idx0, idx1, idx2], True) + + # If both end states contain a dummy, the use null potentials. + if initial_dummy and final_dummy: + theta = _SireCAS.Symbol("theta") + null_angle = _SireMM.AmberAngle(0.0, theta).to_expression(theta) + new_angles0.set(idx0, idx1, idx2, null_angle) + new_angles1.set(idx0, idx1, idx2, null_angle) + # If the initial state contains a dummy, then use the potential from the final state. + # This should already be the case, but we explicitly set it here. + elif initial_dummy: + new_angles0.set(idx0, idx1, idx2, p1.function()) + new_angles1.set(idx0, idx1, idx2, p1.function()) + # If the final state contains a dummy, then use the potential from the initial state. + # This should already be the case, but we explicitly set it here. + elif final_dummy: + new_angles0.set(idx0, idx1, idx2, p0.function()) + new_angles1.set(idx0, idx1, idx2, p0.function()) + # Otherwise, use the potentials from the initial and final states. + else: + new_angles0.set(idx0, idx1, idx2, p0.function()) + new_angles1.set(idx0, idx1, idx2, p1.function()) + + # Set the new angle terms. + edit_mol = edit_mol.set_property("angle0", new_angles0).molecule() + edit_mol = edit_mol.set_property("angle1", new_angles1).molecule() + + ############################ + # Now process the dihedrals. + ############################ + + new_dihedrals0 = _SireMM.FourAtomFunctions(mol.info()) + new_dihedrals1 = _SireMM.FourAtomFunctions(mol.info()) + + # Extract the dihedrals at lambda = 0 and 1. + dihedrals0 = mol.property("dihedral0").potentials() + dihedrals1 = mol.property("dihedral1").potentials() + + # Dictionaries to store the DihedralIDs at lambda = 0 and 1. + dihedrals0_idx = {} + dihedrals1_idx = {} + + # Loop over all dihedrals at lambda = 0. + for idx, dihedral in enumerate(dihedrals0): + # Get the AtomIdx for the atoms in the dihedral. + idx0 = info.atom_idx(dihedral.atom0()) + idx1 = info.atom_idx(dihedral.atom1()) + idx2 = info.atom_idx(dihedral.atom2()) + idx3 = info.atom_idx(dihedral.atom3()) + + # Create the DihedralID. + dihedral_id = _SireMol.DihedralID(idx0, idx1, idx2, idx3) + + # Add to the list of ids. + dihedrals0_idx[dihedral_id] = idx + + # Loop over all dihedrals at lambda = 1. + for idx, dihedral in enumerate(dihedrals1): + # Get the AtomIdx for the atoms in the dihedral. + idx0 = info.atom_idx(dihedral.atom0()) + idx1 = info.atom_idx(dihedral.atom1()) + idx2 = info.atom_idx(dihedral.atom2()) + idx3 = info.atom_idx(dihedral.atom3()) + + # Create the DihedralID. + dihedral_id = _SireMol.DihedralID(idx0, idx1, idx2, idx3) + + # Add to the list of ids. + if dihedral_id.mirror() in dihedrals0_idx: + dihedrals1_idx[dihedral_id.mirror()] = idx + else: + dihedrals1_idx[dihedral_id] = idx + + # Now work out the DihedralIDs that are unique at lambda = 0 and 1 + # as well as those that are shared. + dihedrals0_unique_idx = {} + dihedrals1_unique_idx = {} + dihedrals_shared_idx = {} + + # lambda = 0. + for idx in dihedrals0_idx.keys(): + if idx not in dihedrals1_idx.keys(): + dihedrals0_unique_idx[idx] = dihedrals0_idx[idx] + else: + dihedrals_shared_idx[idx] = (dihedrals0_idx[idx], dihedrals1_idx[idx]) + + # lambda = 1. + for idx in dihedrals1_idx.keys(): + if idx not in dihedrals0_idx.keys(): + dihedrals1_unique_idx[idx] = dihedrals1_idx[idx] + elif idx not in dihedrals_shared_idx.keys(): + dihedrals_shared_idx[idx] = (dihedrals0_idx[idx], dihedrals1_idx[idx]) + + # Loop over the dihedrals. + for idx0, idx1 in dihedrals_shared_idx.values(): + # Get the dihedral potentials. + p0 = dihedrals0[idx0] + p1 = dihedrals1[idx1] + + # Get the AtomIdx for the atoms in the dihedral. + idx0 = info.atom_idx(p0.atom0()) + idx1 = info.atom_idx(p0.atom1()) + idx2 = info.atom_idx(p0.atom2()) + idx3 = info.atom_idx(p0.atom3()) + + # Whether any atom in each end state is a dummy. + has_dummy_initial = _has_dummy(mol, [idx0, idx1, idx2, idx3]) + has_dummy_final = _has_dummy(mol, [idx0, idx1, idx2, idx3], True) + + # Whether all atoms in each state are dummies. + all_dummy_initial = all(_is_dummy(mol, [idx0, idx1, idx2, idx3])) + all_dummy_final = all(_is_dummy(mol, [idx0, idx1, idx2, idx3], True)) + + # If both end states contain a dummy, the use null potentials. + if has_dummy_initial and has_dummy_final: + phi = _SireCAS.Symbol("phi") + null_dihedral = _SireMM.AmberDihedral(0.0, phi).to_expression(phi) + new_dihedrals0.set(idx0, idx1, idx2, idx3, null_dihedral) + new_dihedrals1.set(idx0, idx1, idx2, idx3, null_dihedral) + elif has_dummy_initial: + # If all the atoms are dummy, then use the potential from the final state. + if all_dummy_initial: + new_dihedrals0.set(idx0, idx1, idx2, idx3, p1.function()) + new_dihedrals1.set(idx0, idx1, idx2, idx3, p1.function()) + # Otherwise, zero the potential. + else: + phi = _SireCAS.Symbol("phi") + null_dihedral = _SireMM.AmberDihedral(0.0, phi).to_expression(phi) + new_dihedrals0.set(idx0, idx1, idx2, idx3, null_dihedral) + new_dihedrals1.set(idx0, idx1, idx2, idx3, p1.function()) + elif has_dummy_final: + # If all the atoms are dummy, then use the potential from the initial state. + if all_dummy_final: + new_dihedrals0.set(idx0, idx1, idx2, idx3, p0.function()) + new_dihedrals1.set(idx0, idx1, idx2, idx3, p0.function()) + # Otherwise, zero the potential. + else: + phi = _SireCAS.Symbol("phi") + null_dihedral = _SireMM.AmberDihedral(0.0, phi).to_expression(phi) + new_dihedrals0.set(idx0, idx1, idx2, idx3, p0.function()) + new_dihedrals1.set(idx0, idx1, idx2, idx3, null_dihedral) + else: + new_dihedrals0.set(idx0, idx1, idx2, idx3, p0.function()) + new_dihedrals1.set(idx0, idx1, idx2, idx3, p1.function()) + + # Set the new dihedral terms. + edit_mol = edit_mol.set_property("dihedral0", new_dihedrals0).molecule() + edit_mol = edit_mol.set_property("dihedral1", new_dihedrals1).molecule() + + ############################ + # Now process the impropers. + ############################ + + new_impropers0 = _SireMM.FourAtomFunctions(mol.info()) + new_impropers1 = _SireMM.FourAtomFunctions(mol.info()) + + # Extract the impropers at lambda = 0 and 1. + impropers0 = mol.property("improper0").potentials() + impropers1 = mol.property("improper1").potentials() + + # Dictionaries to store the ImproperIDs at lambda = 0 and 1. + impropers0_idx = {} + impropers1_idx = {} + + # Loop over all impropers at lambda = 0. + for idx, improper in enumerate(impropers0): + # Get the AtomIdx for the atoms in the improper. + idx0 = info.atom_idx(improper.atom0()) + idx1 = info.atom_idx(improper.atom1()) + idx2 = info.atom_idx(improper.atom2()) + idx3 = info.atom_idx(improper.atom3()) + + # Create the ImproperID. + improper_id = _SireMol.ImproperID(idx0, idx1, idx2, idx3) + + # Add to the list of ids. + impropers0_idx[improper_id] = idx + + # Loop over all impropers at lambda = 1. + for idx, improper in enumerate(impropers1): + # Get the AtomIdx for the atoms in the improper. + idx0 = info.atom_idx(improper.atom0()) + idx1 = info.atom_idx(improper.atom1()) + idx2 = info.atom_idx(improper.atom2()) + idx3 = info.atom_idx(improper.atom3()) + + # Create the ImproperID. + improper_id = _SireMol.ImproperID(idx0, idx1, idx2, idx3) + + # Add to the list of ids. + # You cannot mirror an improper! + impropers1_idx[improper_id] = idx + + # Now work out the ImproperIDs that are unique at lambda = 0 and 1 + # as well as those that are shared. Note that the ordering of + # impropers is inconsistent between molecular topology formats so + # we test all permutations of atom ordering to find matches. This + # is achieved using the ImproperID.equivalent() method. + impropers0_unique_idx = {} + impropers1_unique_idx = {} + impropers_shared_idx = {} + + # lambda = 0. + for idx0 in impropers0_idx.keys(): + for idx1 in impropers1_idx.keys(): + if idx0.equivalent(idx1): + impropers_shared_idx[idx0] = ( + impropers0_idx[idx0], + impropers1_idx[idx1], + ) + break + else: + impropers0_unique_idx[idx0] = impropers0_idx[idx0] + + # lambda = 1. + for idx1 in impropers1_idx.keys(): + for idx0 in impropers0_idx.keys(): + if idx1.equivalent(idx0): + # Don't store duplicates. + if not idx0 in impropers_shared_idx.keys(): + impropers_shared_idx[idx1] = ( + impropers0_idx[idx0], + impropers1_idx[idx1], + ) + break + else: + impropers1_unique_idx[idx1] = impropers1_idx[idx1] + + # Loop over the impropers. + for idx0, idx1 in impropers_shared_idx.values(): + # Get the improper potentials. + p0 = impropers0[idx0] + p1 = impropers1[idx1] + + # Get the AtomIdx for the atoms in the dihedral. + idx0 = info.atom_idx(p0.atom0()) + idx1 = info.atom_idx(p0.atom1()) + idx2 = info.atom_idx(p0.atom2()) + idx3 = info.atom_idx(p0.atom3()) + + # Whether any atom in each end state is a dummy. + has_dummy_initial = _has_dummy(mol, [idx0, idx1, idx2, idx3]) + has_dummy_final = _has_dummy(mol, [idx0, idx1, idx2, idx3], True) + + # Whether all atoms in each state are dummies. + all_dummy_initial = all(_is_dummy(mol, [idx0, idx1, idx2, idx3])) + all_dummy_final = all(_is_dummy(mol, [idx0, idx1, idx2, idx3], True)) + + if has_dummy_initial and has_dummy_final: + phi = _SireCAS.Symbol("phi") + null_dihedral = _SireMM.AmberDihedral(0.0, phi).to_expression(phi) + new_impropers0.set(idx0, idx1, idx2, idx3, null_dihedral) + new_impropers1.set(idx0, idx1, idx2, idx3, null_dihedral) + elif has_dummy_initial: + # If all the atoms are dummy, then use the potential from the final state. + if all_dummy_initial: + new_impropers0.set(idx0, idx1, idx2, idx3, p1.function()) + new_impropers1.set(idx0, idx1, idx2, idx3, p1.function()) + # Otherwise, zero the potential. + else: + phi = _SireCAS.Symbol("phi") + null_dihedral = _SireMM.AmberDihedral(0.0, phi).to_expression(phi) + new_impropers0.set(idx0, idx1, idx2, idx3, null_dihedral) + new_impropers1.set(idx0, idx1, idx2, idx3, p1.function()) + elif has_dummy_final: + # If all the atoms are dummy, then use the potential from the initial state. + if all_dummy_final: + new_impropers0.set(idx0, idx1, idx2, idx3, p0.function()) + new_impropers1.set(idx0, idx1, idx2, idx3, p0.function()) + # Otherwise, zero the potential. + else: + phi = _SireCAS.Symbol("phi") + null_dihedral = _SireMM.AmberDihedral(0.0, phi).to_expression(phi) + new_impropers0.set(idx0, idx1, idx2, idx3, p0.function()) + new_impropers1.set(idx0, idx1, idx2, idx3, null_dihedral) + else: + new_impropers0.set(idx0, idx1, idx2, idx3, p0.function()) + new_impropers1.set(idx0, idx1, idx2, idx3, p1.function()) + + # Set the new improper terms. + edit_mol = edit_mol.set_property("improper0", new_impropers0).molecule() + edit_mol = edit_mol.set_property("improper1", new_impropers1).molecule() + + # Commit the changes and update the molecule in the system. + system._sire_object.update(edit_mol.commit()) + + # Return the updated system. + return system From b87d1ed1ade50cc2bdf3aa046c68d0d40e94379e Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 14 Mar 2024 13:04:42 +0000 Subject: [PATCH 052/121] Need to pass system to hasWater method. --- python/BioSimSpace/_Config/_somd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/BioSimSpace/_Config/_somd.py b/python/BioSimSpace/_Config/_somd.py index 7ee18fa7a..a5ba74106 100644 --- a/python/BioSimSpace/_Config/_somd.py +++ b/python/BioSimSpace/_Config/_somd.py @@ -173,7 +173,7 @@ def createConfig(self, extra_options={}, extra_lines=[]): pass # Periodic boundary conditions. - if self.hasWater(): + if self.hasWater(self._system): # Solvated box. protocol_dict["reaction field dielectric"] = "78.3" if not self.hasBox(self._system, self._property_map) or not self.hasWater( From 70f309c328380e8192d01b64c3b0604b412b0b5e Mon Sep 17 00:00:00 2001 From: Miroslav Suruzhon <36005076+msuruzhon@users.noreply.github.com> Date: Thu, 21 Mar 2024 10:05:05 +0000 Subject: [PATCH 053/121] Merge remote-tracking branch 'obs/main' into feat_main (#41) --- .github/workflows/Sandpit_exs.yml | 2 +- doc/source/changelog.rst | 13 ++ python/BioSimSpace/Process/_gromacs.py | 6 +- .../Protocol/_position_restraint_mixin.py | 4 +- .../Exscientia/FreeEnergy/_restraint.py | 10 +- .../FreeEnergy/_restraint_search.py | 4 +- .../Sandpit/Exscientia/Process/_gromacs.py | 6 +- .../Protocol/_position_restraint.py | 2 +- .../Sandpit/Exscientia/Types/_angle.py | 18 +- .../Sandpit/Exscientia/Types/_area.py | 18 +- .../Sandpit/Exscientia/Types/_charge.py | 16 +- .../Sandpit/Exscientia/Types/_energy.py | 16 +- .../Sandpit/Exscientia/Types/_general_unit.py | 170 ++++++++++-------- .../Sandpit/Exscientia/Types/_length.py | 41 +---- .../Sandpit/Exscientia/Types/_pressure.py | 16 +- .../Sandpit/Exscientia/Types/_temperature.py | 33 ++-- .../Sandpit/Exscientia/Types/_time.py | 24 +-- .../Sandpit/Exscientia/Types/_type.py | 110 ++++++------ .../Sandpit/Exscientia/Types/_volume.py | 18 +- .../Exscientia/_SireWrappers/_molecule.py | 15 +- python/BioSimSpace/Types/_angle.py | 18 +- python/BioSimSpace/Types/_area.py | 18 +- python/BioSimSpace/Types/_charge.py | 16 +- python/BioSimSpace/Types/_energy.py | 16 +- python/BioSimSpace/Types/_general_unit.py | 170 ++++++++++-------- python/BioSimSpace/Types/_length.py | 41 +---- python/BioSimSpace/Types/_pressure.py | 16 +- python/BioSimSpace/Types/_temperature.py | 28 +-- python/BioSimSpace/Types/_time.py | 24 +-- python/BioSimSpace/Types/_type.py | 110 ++++++------ python/BioSimSpace/Types/_volume.py | 18 +- python/BioSimSpace/_Config/_amber.py | 6 +- python/BioSimSpace/_SireWrappers/_molecule.py | 15 +- recipes/biosimspace/template.yaml | 11 +- requirements.txt | 2 +- .../FreeEnergy/test_restraint_search.py | 10 +- .../Process/test_position_restraint.py | 1 + .../Exscientia/Types/test_general_unit.py | 76 +++++++- tests/Types/test_general_unit.py | 76 +++++++- 39 files changed, 676 insertions(+), 538 deletions(-) diff --git a/.github/workflows/Sandpit_exs.yml b/.github/workflows/Sandpit_exs.yml index 76b9a3242..f5b0fc0d3 100644 --- a/.github/workflows/Sandpit_exs.yml +++ b/.github/workflows/Sandpit_exs.yml @@ -35,7 +35,7 @@ jobs: - name: Install dependency run: | - mamba install -c conda-forge -c openbiosim/label/main biosimspace python=3.10 ambertools gromacs "sire=2023.4" "alchemlyb>=2.1" pytest openff-interchange pint=0.21 rdkit "jaxlib>0.3.7" tqdm + mamba install -c conda-forge -c openbiosim/label/main biosimspace python=3.10 ambertools gromacs "sire=2023.5" "alchemlyb>=2.1" pytest openff-interchange pint=0.21 rdkit "jaxlib>0.3.7" tqdm python -m pip install git+https://github.com/Exscientia/MDRestraintsGenerator.git # For the testing of BSS.FreeEnergy.AlchemicalFreeEnergy.analysis python -m pip install https://github.com/alchemistry/alchemtest/archive/master.zip diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 5420e438a..0e8765d6c 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -9,6 +9,19 @@ company supporting open-source development of fostering academic/industrial coll within the biomolecular simulation community. Our software is hosted via the `OpenBioSim` `GitHub `__ organisation. +`2023.5.1 `_ - Mar 20 2024 +------------------------------------------------------------------------------------------------- + +* Fixed path to user links file in the :func:`generateNetwork ` function (`#233 `__). +* Fixed redirection of stderr (`#233 `__). +* Switched to using ``AtomCoordMatcher`` to map parameterised molecules back to their original topology. This resolves issues where atoms moved between residues following parameterisation (`#235 `__). +* Make the GROMACS ``_generate_binary_run_file`` function static so that it can be used when initialising free energy simulations in setup-only mode (`#237 `__). +* Improve error handling and message when attempting to extract an all dummy atom selection (`#251 `__). +* Don't set SOMD specific end-state properties when decoupling a molecule (`#253 `__). +* Only convert to a end-state system when not running a free energy protocol with GROMACS so that hybrid topology isn't lost when using position restraints (`#257 `__). +* Exclude standard free ions from the AMBER position restraint mask (`#260 `__). +* Update the ``BioSimSpace.Types._GeneralUnit.__pow__`` operator to support fractional exponents (`#260 `__). + `2023.5.0 `_ - Dec 16 2023 ------------------------------------------------------------------------------------------------- diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index e4531dad3..166910e05 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -1995,8 +1995,10 @@ def _add_position_restraints(self): # Create a copy of the system. system = self._system.copy() - # Convert to the lambda = 0 state if this is a perturbable system. - system = self._checkPerturbable(system) + # Convert to the lambda = 0 state if this is a perturbable system and this + # isn't a free energy protocol. + if not isinstance(self._protocol, _FreeEnergyMixin): + system = self._checkPerturbable(system) # Convert the water model topology so that it matches the GROMACS naming convention. system._set_water_topology("GROMACS") diff --git a/python/BioSimSpace/Protocol/_position_restraint_mixin.py b/python/BioSimSpace/Protocol/_position_restraint_mixin.py index 346f7f6e3..8374bf8ad 100644 --- a/python/BioSimSpace/Protocol/_position_restraint_mixin.py +++ b/python/BioSimSpace/Protocol/_position_restraint_mixin.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -214,7 +214,7 @@ def setForceConstant(self, force_constant): ) # Validate the dimensions. - if force_constant.dimensions() != (0, 0, 0, 1, -1, 0, -2): + if force_constant.dimensions() != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "'force_constant' has invalid dimensions! " f"Expected dimensions are 'M Q-1 T-2', found '{force_constant.unit()}'" diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py index 5aaa857db..54421eed9 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -199,7 +199,7 @@ def __init__(self, system, restraint_dict, temperature, restraint_type="Boresch" for key in ["kthetaA", "kthetaB", "kphiA", "kphiB", "kphiC"]: if restraint_dict["force_constants"][key] != 0: dim = restraint_dict["force_constants"][key].dimensions() - if dim != (-2, 0, 2, 1, -1, 0, -2): + if dim != (1, 2, -2, 0, 0, -1, -2): raise ValueError( f"restraint_dict['force_constants']['{key}'] must be of type " f"'BioSimSpace.Types.Energy'/'BioSimSpace.Types.Angle^2'" @@ -227,7 +227,7 @@ def __init__(self, system, restraint_dict, temperature, restraint_type="Boresch" # Test if the force constant of the bond r1-l1 is the correct unit # Such as kcal/mol/angstrom^2 dim = restraint_dict["force_constants"]["kr"].dimensions() - if dim != (0, 0, 0, 1, -1, 0, -2): + if dim != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "restraint_dict['force_constants']['kr'] must be of type " "'BioSimSpace.Types.Energy'/'BioSimSpace.Types.Length^2'" @@ -315,13 +315,13 @@ def __init__(self, system, restraint_dict, temperature, restraint_type="Boresch" "'BioSimSpace.Types.Length'" ) if not single_restraint_dict["kr"].dimensions() == ( + 1, 0, + -2, 0, 0, - 1, -1, 0, - -2, ): raise ValueError( "distance_restraint_dict['kr'] must be of type " diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py index 30c4b0af4..f7ec62dd4 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -641,7 +641,7 @@ def analyse( if force_constant: dim = force_constant.dimensions() - if dim != (0, 0, 0, 1, -1, 0, -2): + if dim != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "force_constant must be of type " "'BioSimSpace.Types.Energy'/'BioSimSpace.Types.Length^2'" diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py index 5953781fc..788450c84 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_gromacs.py @@ -2102,8 +2102,10 @@ def _add_position_restraints(self, config_options): # Create a copy of the system. system = self._system.copy() - # Convert to the lambda = 0 state if this is a perturbable system. - system = self._checkPerturbable(system) + # Convert to the lambda = 0 state if this is a perturbable system and this + # isn't a free energy protocol. + if not isinstance(self._protocol, _Protocol._FreeEnergyMixin): + system = self._checkPerturbable(system) # Convert the water model topology so that it matches the GROMACS naming convention. system._set_water_topology("GROMACS") diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py index ae3578870..38a82266d 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_position_restraint.py @@ -185,7 +185,7 @@ def setForceConstant(self, force_constant): ) # Validate the dimensions. - if force_constant.dimensions() != (0, 0, 0, 1, -1, 0, -2): + if force_constant.dimensions() != (1, 0, -2, 0, 0, -1, 0): raise ValueError( "'force_constant' has invalid dimensions! " f"Expected dimensions are 'M Q-1 T-2', found '{force_constant.unit()}'" diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py index 759a71724..cef19c862 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_angle.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -52,9 +52,8 @@ class Angle(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "RADIAN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (1, 0, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -188,7 +187,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -210,13 +210,13 @@ def _validate_unit(self, unit): unit = unit.replace("AD", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py index ec8484cca..2763618a9 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_area.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -72,9 +72,8 @@ class Area(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM2" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -330,7 +329,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit is supported.""" # Strip whitespace and convert to upper case. @@ -360,13 +360,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "2" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py index 7717f481e..a65d54cd3 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_charge.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -58,9 +58,8 @@ class Charge(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ELECTRON CHARGE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 1, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -182,7 +181,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -213,11 +213,11 @@ def _validate_unit(self, unit): unit = unit.replace("COUL", "C") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py index bb293a17a..af9aa2894 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_energy.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -68,9 +68,8 @@ class Energy(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KILO CALORIES PER MOL" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 1, -1, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -213,7 +212,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -235,11 +235,11 @@ def _validate_unit(self, unit): unit = unit.replace("JOULES", "J") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py index deca60800..7097e616e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_general_unit.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -38,16 +38,16 @@ class GeneralUnit(_Type): """A general unit type.""" _dimension_chars = [ - "A", # Angle - "C", # Charge - "L", # Length "M", # Mass - "Q", # Quantity + "L", # Length + "T", # Time + "C", # Charge "t", # Temperature - "T", # Tme + "Q", # Quantity + "A", # Angle ] - def __new__(cls, *args): + def __new__(cls, *args, no_cast=False): """ Constructor. @@ -65,6 +65,9 @@ def __new__(cls, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ # This operator may be called when unpickling an object. Catch empty @@ -96,7 +99,7 @@ def __new__(cls, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -128,15 +131,7 @@ def __new__(cls, *args): general_unit = value * general_unit # Store the dimension mask. - dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + dimensions = tuple(general_unit.dimensions()) # This is a dimensionless quantity, return the value as a float. if all(x == 0 for x in dimensions): @@ -144,13 +139,13 @@ def __new__(cls, *args): # Check to see if the dimensions correspond to a supported type. # If so, return an object of that type. - if dimensions in _base_dimensions: + if not no_cast and dimensions in _base_dimensions: return _base_dimensions[dimensions](general_unit) # Otherwise, call __init__() else: return super(GeneralUnit, cls).__new__(cls) - def __init__(self, *args): + def __init__(self, *args, no_cast=False): """ Constructor. @@ -168,6 +163,9 @@ def __init__(self, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ value = 1 @@ -194,7 +192,7 @@ def __init__(self, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -222,15 +220,7 @@ def __init__(self, *args): self._value = self._sire_unit.value() # Store the dimension mask. - self._dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + self._dimensions = tuple(general_unit.dimensions()) # Create the unit string. self._unit = "" @@ -271,12 +261,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -285,17 +285,27 @@ def __sub__(self, other): temp = self._sire_unit - other._to_sire_unit() return GeneralUnit(temp) - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" @@ -312,16 +322,8 @@ def __mul__(self, other): # Multipy the Sire unit objects. temp = self._sire_unit * other._to_sire_unit() - # Create the dimension mask. - dimensions = ( - temp.ANGLE(), - temp.CHARGE(), - temp.LENGTH(), - temp.MASS(), - temp.QUANTITY(), - temp.TEMPERATURE(), - temp.TIME(), - ) + # Get the dimension mask. + dimensions = temp.dimensions() # Return as an existing type if the dimensions match. try: @@ -432,7 +434,7 @@ def __rtruediv__(self, other): def __pow__(self, other): """Power operator.""" - if type(other) is not int: + if not isinstance(other, (int, float)): raise TypeError( "unsupported operand type(s) for ^: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) @@ -441,15 +443,29 @@ def __pow__(self, other): if other == 0: return GeneralUnit(self._sire_unit / self._sire_unit) - # Multiply the Sire GeneralUnit 'other' times. - temp = self._sire_unit - for x in range(0, abs(other) - 1): - temp = temp * self._sire_unit + # Convert to float. + other = float(other) - if other > 0: - return GeneralUnit(temp) - else: - return GeneralUnit(1 / temp) + # Get the existing unit dimensions. + dims = self.dimensions() + + # Compute the new dimensions, rounding floats to 16 decimal places. + new_dims = [round(dim * other, 16) for dim in dims] + + # Make sure the new dimensions are integers. + if not all(dim.is_integer() for dim in new_dims): + raise ValueError( + "The exponent must be a factor of all the unit dimensions." + ) + + # Convert to integers. + new_dims = [int(dim) for dim in new_dims] + + # Compute the new value. + value = self.value() ** other + + # Return a new GeneralUnit object. + return GeneralUnit(_GeneralUnit(value, new_dims)) def __lt__(self, other): """Less than operator.""" @@ -606,87 +622,87 @@ def dimensions(self): """ return self._dimensions - def angle(self): + def mass(self): """ - Return the power of this general unit in the 'angle' dimension. + Return the power of this general unit in the 'mass' dimension. Returns ------- - angle : int - The power of the general unit in the 'angle' dimension. + mass : int + The power of the general unit in the 'mass' dimension. """ return self._dimensions[0] - def charge(self): + def length(self): """ - Return the power of this general unit in the 'charge' dimension. + Return the power of this general unit in the 'length' dimension. Returns ------- - charge : int - The power of the general unit in the 'charge' dimension. + length : int + The power of the general unit in the 'length' dimension. """ return self._dimensions[1] - def length(self): + def time(self): """ - Return the power of this general unit in the 'length' dimension. + Return the power of this general unit in the 'time' dimension. Returns ------- - length : int - The power of the general unit in the 'length' dimension. + time : int + The power of the general unit in the 'time' dimension. """ return self._dimensions[2] - def mass(self): + def charge(self): """ - Return the power of this general unit in the 'mass' dimension. + Return the power of this general unit in the 'charge' dimension. Returns ------- - mass : int - The power of the general unit in the 'mass' dimension. + charge : int + The power of the general unit in the 'charge' dimension. """ return self._dimensions[3] - def quantity(self): + def temperature(self): """ - Return the power of this general unit in the 'quantity' dimension. + Return the power of this general unit in the 'temperature' dimension. Returns ------- - quantity : int - The power of the general unit in the 'quantity' dimension. + temperature : int + The power of the general unit in the 'temperature' dimension. """ return self._dimensions[4] - def temperature(self): + def quantity(self): """ - Return the power of this general unit in the 'temperature' dimension. + Return the power of this general unit in the 'quantity' dimension. Returns ------- - temperature : int - The power of the general unit in the 'temperature' dimension. + quantity : int + The power of the general unit in the 'quantity' dimension. """ return self._dimensions[5] - def time(self): + def angle(self): """ - Return the power of this general unit in the 'time' dimension. + Return the power of this general unit in the 'angle' dimension. Returns ------- - time : int - The power of the general unit in the 'time' dimension. + angle : int + The power of the general unit in the 'angle' dimension. """ return self._dimensions[6] diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py index 5eb10fb07..67f163af8 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_length.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -87,9 +87,8 @@ class Length(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 1, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -195,29 +194,6 @@ def __rmul__(self, other): # Multiplication is commutative: a*b = b*a return self.__mul__(other) - def __pow__(self, other): - """Power operator.""" - - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - - # No change. - if other == 1: - return self - - # Area. - if other == 2: - mag = self.angstroms().value() ** 2 - return _Area(mag, "A2") - - # Volume. - if other == 3: - mag = self.angstroms().value() ** 3 - return _Volume(mag, "A3") - - else: - return super().__pow__(other) - def meters(self): """ Return the length in meters. @@ -362,7 +338,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -376,13 +353,13 @@ def _validate_unit(self, unit): unit = "ANGS" + unit[3:] # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py index 699d9d5f2..fbe6da782 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_pressure.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -55,9 +55,8 @@ class Pressure(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ATMOSPHERE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, -1, 1, 0, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -177,7 +176,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -196,11 +196,11 @@ def _validate_unit(self, unit): unit = unit.replace("S", "") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py index d11d70f0c..a7010dd7f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_temperature.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -60,9 +60,8 @@ class Temperature(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KELVIN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 1, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -392,7 +391,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -405,13 +405,16 @@ def _validate_unit(self, unit): unit = unit.replace("DEG", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] + elif len(unit) == 0: + raise ValueError(f"Unit is not given. You must supply the unit.") else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Unsupported unit '%s'. Supported units are: '%s'" + % (unit, list(cls._supported_units.keys())) ) def _to_sire_unit(self): @@ -441,13 +444,13 @@ def _from_sire_unit(cls, sire_unit): if isinstance(sire_unit, _SireUnits.GeneralUnit): # Create a mask for the dimensions of the object. dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), + sire_unit.LENGTH(), sire_unit.TIME(), + sire_unit.CHARGE(), + sire_unit.TEMPERATURE(), + sire_unit.QUANTITY(), + sire_unit.ANGLE(), ) # Make sure the dimensions match. @@ -470,7 +473,7 @@ def _from_sire_unit(cls, sire_unit): else: raise TypeError( "'sire_unit' must be of type 'sire.units.GeneralUnit', " - "'Sire.Units.Celsius', or 'sire.units.Fahrenheit'" + "'sire.units.Celsius', or 'sire.units.Fahrenheit'" ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py index 19fc10401..0f8dda977 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_time.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -96,9 +96,8 @@ class Time(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "NANOSECOND" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 0, 1) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -337,24 +336,25 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. unit = unit.replace(" ", "").upper() # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit[:-1] in self._supported_units: + elif unit[:-1] in cls._supported_units: return unit[:-1] - elif unit in self._abbreviations: - return self._abbreviations[unit] - elif unit[:-1] in self._abbreviations: - return self._abbreviations[unit[:-1]] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] + elif unit[:-1] in cls._abbreviations: + return cls._abbreviations[unit[:-1]] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py index f01635631..75d7b7e0c 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_type.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -103,7 +103,7 @@ def __init__(self, *args): self._value = temp._value self._unit = temp._unit - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(args[0], str): # Convert the string to an object of this type. obj = self._from_string(args[0]) @@ -168,12 +168,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -185,22 +195,32 @@ def __sub__(self, other): # Return a new object of the same type with the original unit. return self._to_default_unit(val)._convert_to(self._unit) - # Addition of a different type with the same dimensions. + # Subtraction of a different type with the same dimensions. elif isinstance(other, Type) and self._dimensions == other.dimensions: # Negate other and add. return -other + self - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" @@ -244,19 +264,9 @@ def __rmul__(self, other): def __pow__(self, other): """Power operator.""" - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - from ._general_unit import GeneralUnit as _GeneralUnit - default_unit = self._to_default_unit() - mag = default_unit.value() ** other - unit = default_unit.unit().lower() - pow_to_mul = "*".join(abs(other) * [unit]) - if other > 0: - return _GeneralUnit(f"{mag}*{pow_to_mul}") - else: - return _GeneralUnit(f"{mag}/({pow_to_mul})") + return _GeneralUnit(self._to_sire_unit(), no_cast=True) ** other def __truediv__(self, other): """Division operator.""" @@ -486,99 +496,99 @@ def dimensions(cls): containing the power in each dimension. Returns : (int, int, int, int, int, int) - The power in each dimension: 'angle', 'charge', 'length', - 'mass', 'quantity', 'temperature', and 'time'. + The power in each dimension: 'mass', 'length', 'temperature', + 'charge', 'time', 'quantity', and 'angle'. """ return cls._dimensions @classmethod - def angle(cls): + def mass(cls): """ - Return the power in the 'angle' dimension. + Return the power in the 'mass' dimension. Returns ------- - angle : int - The power in the 'angle' dimension. + mass : int + The power in the 'mass' dimension. """ return cls._dimensions[0] @classmethod - def charge(cls): + def length(cls): """ - Return the power in the 'charge' dimension. + Return the power in the 'length' dimension. Returns ------- - charge : int - The power in the 'charge' dimension. + length : int + The power in the 'length' dimension. """ return cls._dimensions[1] @classmethod - def length(cls): + def time(cls): """ - Return the power in the 'length' dimension. + Return the power in the 'time' dimension. Returns ------- - length : int - The power in the 'length' dimension. + time : int + The power the 'time' dimension. """ return cls._dimensions[2] @classmethod - def mass(cls): + def charge(cls): """ - Return the power in the 'mass' dimension. + Return the power in the 'charge' dimension. Returns ------- - mass : int - The power in the 'mass' dimension. + charge : int + The power in the 'charge' dimension. """ return cls._dimensions[3] @classmethod - def quantity(cls): + def temperature(cls): """ - Return the power in the 'quantity' dimension. + Return the power in the 'temperature' dimension. Returns ------- - quantity : int - The power in the 'quantity' dimension. + temperature : int + The power in the 'temperature' dimension. """ return cls._dimensions[4] @classmethod - def temperature(cls): + def quantity(cls): """ - Return the power in the 'temperature' dimension. + Return the power in the 'quantity' dimension. Returns ------- - temperature : int - The power in the 'temperature' dimension. + quantity : int + The power in the 'quantity' dimension. """ return cls._dimensions[5] @classmethod - def time(cls): + def angle(cls): """ - Return the power in the 'time' dimension. + Return the power in the 'angle' dimension. Returns ------- - time : int - The power the 'time' dimension. + angle : int + The power in the 'angle' dimension. """ return cls._dimensions[6] @@ -662,15 +672,7 @@ def _from_sire_unit(cls, sire_unit): raise TypeError("'sire_unit' must be of type 'sire.units.GeneralUnit'") # Create a mask for the dimensions of the object. - dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), - sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), - sire_unit.TIME(), - ) + dimensions = tuple(sire_unit.dimensions()) # Make sure that this isn't zero. if hasattr(sire_unit, "is_zero"): diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py index 4b255e01a..4dad85642 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_volume.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -72,9 +72,8 @@ class Volume(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM3" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 3, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -287,7 +286,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -317,13 +317,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "3" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py index 7c25c30f7..dc662f217 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -792,9 +792,8 @@ def makeCompatibleWith( # Have we matched all of the atoms? if len(matches) < num_atoms0: - # Atom names might have changed. Try to match by residue index - # and coordinates. - matcher = _SireMol.ResIdxAtomCoordMatcher() + # Atom names or order might have changed. Try to match by coordinates. + matcher = _SireMol.AtomCoordMatcher() matches = matcher.match(mol0, mol1) # We need to rename the atoms. @@ -994,9 +993,6 @@ def makeCompatibleWith( # Tally counter for the total number of matches. num_matches = 0 - # Initialise the offset. - offset = 0 - # Get the molecule numbers in the system. mol_nums = mol1.molNums() @@ -1006,16 +1002,13 @@ def makeCompatibleWith( mol = mol1[num] # Initialise the matcher. - matcher = _SireMol.ResIdxAtomCoordMatcher(_SireMol.ResIdx(offset)) + matcher = _SireMol.AtomCoordMatcher() # Get the matches for this molecule and append to the list. match = matcher.match(mol0, mol) matches.append(match) num_matches += len(match) - # Increment the offset. - offset += mol.nResidues() - # Have we matched all of the atoms? if num_matches < num_atoms0: raise _IncompatibleError("Failed to match all atoms!") diff --git a/python/BioSimSpace/Types/_angle.py b/python/BioSimSpace/Types/_angle.py index 759a71724..cef19c862 100644 --- a/python/BioSimSpace/Types/_angle.py +++ b/python/BioSimSpace/Types/_angle.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -52,9 +52,8 @@ class Angle(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "RADIAN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (1, 0, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -188,7 +187,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -210,13 +210,13 @@ def _validate_unit(self, unit): unit = unit.replace("AD", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_area.py b/python/BioSimSpace/Types/_area.py index ec8484cca..2763618a9 100644 --- a/python/BioSimSpace/Types/_area.py +++ b/python/BioSimSpace/Types/_area.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -72,9 +72,8 @@ class Area(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM2" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -330,7 +329,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit is supported.""" # Strip whitespace and convert to upper case. @@ -360,13 +360,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "2" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_charge.py b/python/BioSimSpace/Types/_charge.py index 7717f481e..a65d54cd3 100644 --- a/python/BioSimSpace/Types/_charge.py +++ b/python/BioSimSpace/Types/_charge.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -58,9 +58,8 @@ class Charge(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ELECTRON CHARGE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 1, 0, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -182,7 +181,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -213,11 +213,11 @@ def _validate_unit(self, unit): unit = unit.replace("COUL", "C") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_energy.py b/python/BioSimSpace/Types/_energy.py index bb293a17a..af9aa2894 100644 --- a/python/BioSimSpace/Types/_energy.py +++ b/python/BioSimSpace/Types/_energy.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -68,9 +68,8 @@ class Energy(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KILO CALORIES PER MOL" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 2, 1, -1, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -213,7 +212,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -235,11 +235,11 @@ def _validate_unit(self, unit): unit = unit.replace("JOULES", "J") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_general_unit.py b/python/BioSimSpace/Types/_general_unit.py index deca60800..7097e616e 100644 --- a/python/BioSimSpace/Types/_general_unit.py +++ b/python/BioSimSpace/Types/_general_unit.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -38,16 +38,16 @@ class GeneralUnit(_Type): """A general unit type.""" _dimension_chars = [ - "A", # Angle - "C", # Charge - "L", # Length "M", # Mass - "Q", # Quantity + "L", # Length + "T", # Time + "C", # Charge "t", # Temperature - "T", # Tme + "Q", # Quantity + "A", # Angle ] - def __new__(cls, *args): + def __new__(cls, *args, no_cast=False): """ Constructor. @@ -65,6 +65,9 @@ def __new__(cls, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ # This operator may be called when unpickling an object. Catch empty @@ -96,7 +99,7 @@ def __new__(cls, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -128,15 +131,7 @@ def __new__(cls, *args): general_unit = value * general_unit # Store the dimension mask. - dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + dimensions = tuple(general_unit.dimensions()) # This is a dimensionless quantity, return the value as a float. if all(x == 0 for x in dimensions): @@ -144,13 +139,13 @@ def __new__(cls, *args): # Check to see if the dimensions correspond to a supported type. # If so, return an object of that type. - if dimensions in _base_dimensions: + if not no_cast and dimensions in _base_dimensions: return _base_dimensions[dimensions](general_unit) # Otherwise, call __init__() else: return super(GeneralUnit, cls).__new__(cls) - def __init__(self, *args): + def __init__(self, *args, no_cast=False): """ Constructor. @@ -168,6 +163,9 @@ def __init__(self, *args): string : str A string representation of the unit type. + + no_cast: bool + Whether to disable casting to a specific type. """ value = 1 @@ -194,7 +192,7 @@ def __init__(self, *args): if isinstance(_args[0], _GeneralUnit): general_unit = _args[0] - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(_args[0], str): # Extract the string. string = _args[0] @@ -222,15 +220,7 @@ def __init__(self, *args): self._value = self._sire_unit.value() # Store the dimension mask. - self._dimensions = ( - general_unit.ANGLE(), - general_unit.CHARGE(), - general_unit.LENGTH(), - general_unit.MASS(), - general_unit.QUANTITY(), - general_unit.TEMPERATURE(), - general_unit.TIME(), - ) + self._dimensions = tuple(general_unit.dimensions()) # Create the unit string. self._unit = "" @@ -271,12 +261,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -285,17 +285,27 @@ def __sub__(self, other): temp = self._sire_unit - other._to_sire_unit() return GeneralUnit(temp) - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" @@ -312,16 +322,8 @@ def __mul__(self, other): # Multipy the Sire unit objects. temp = self._sire_unit * other._to_sire_unit() - # Create the dimension mask. - dimensions = ( - temp.ANGLE(), - temp.CHARGE(), - temp.LENGTH(), - temp.MASS(), - temp.QUANTITY(), - temp.TEMPERATURE(), - temp.TIME(), - ) + # Get the dimension mask. + dimensions = temp.dimensions() # Return as an existing type if the dimensions match. try: @@ -432,7 +434,7 @@ def __rtruediv__(self, other): def __pow__(self, other): """Power operator.""" - if type(other) is not int: + if not isinstance(other, (int, float)): raise TypeError( "unsupported operand type(s) for ^: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) @@ -441,15 +443,29 @@ def __pow__(self, other): if other == 0: return GeneralUnit(self._sire_unit / self._sire_unit) - # Multiply the Sire GeneralUnit 'other' times. - temp = self._sire_unit - for x in range(0, abs(other) - 1): - temp = temp * self._sire_unit + # Convert to float. + other = float(other) - if other > 0: - return GeneralUnit(temp) - else: - return GeneralUnit(1 / temp) + # Get the existing unit dimensions. + dims = self.dimensions() + + # Compute the new dimensions, rounding floats to 16 decimal places. + new_dims = [round(dim * other, 16) for dim in dims] + + # Make sure the new dimensions are integers. + if not all(dim.is_integer() for dim in new_dims): + raise ValueError( + "The exponent must be a factor of all the unit dimensions." + ) + + # Convert to integers. + new_dims = [int(dim) for dim in new_dims] + + # Compute the new value. + value = self.value() ** other + + # Return a new GeneralUnit object. + return GeneralUnit(_GeneralUnit(value, new_dims)) def __lt__(self, other): """Less than operator.""" @@ -606,87 +622,87 @@ def dimensions(self): """ return self._dimensions - def angle(self): + def mass(self): """ - Return the power of this general unit in the 'angle' dimension. + Return the power of this general unit in the 'mass' dimension. Returns ------- - angle : int - The power of the general unit in the 'angle' dimension. + mass : int + The power of the general unit in the 'mass' dimension. """ return self._dimensions[0] - def charge(self): + def length(self): """ - Return the power of this general unit in the 'charge' dimension. + Return the power of this general unit in the 'length' dimension. Returns ------- - charge : int - The power of the general unit in the 'charge' dimension. + length : int + The power of the general unit in the 'length' dimension. """ return self._dimensions[1] - def length(self): + def time(self): """ - Return the power of this general unit in the 'length' dimension. + Return the power of this general unit in the 'time' dimension. Returns ------- - length : int - The power of the general unit in the 'length' dimension. + time : int + The power of the general unit in the 'time' dimension. """ return self._dimensions[2] - def mass(self): + def charge(self): """ - Return the power of this general unit in the 'mass' dimension. + Return the power of this general unit in the 'charge' dimension. Returns ------- - mass : int - The power of the general unit in the 'mass' dimension. + charge : int + The power of the general unit in the 'charge' dimension. """ return self._dimensions[3] - def quantity(self): + def temperature(self): """ - Return the power of this general unit in the 'quantity' dimension. + Return the power of this general unit in the 'temperature' dimension. Returns ------- - quantity : int - The power of the general unit in the 'quantity' dimension. + temperature : int + The power of the general unit in the 'temperature' dimension. """ return self._dimensions[4] - def temperature(self): + def quantity(self): """ - Return the power of this general unit in the 'temperature' dimension. + Return the power of this general unit in the 'quantity' dimension. Returns ------- - temperature : int - The power of the general unit in the 'temperature' dimension. + quantity : int + The power of the general unit in the 'quantity' dimension. """ return self._dimensions[5] - def time(self): + def angle(self): """ - Return the power of this general unit in the 'time' dimension. + Return the power of this general unit in the 'angle' dimension. Returns ------- - time : int - The power of the general unit in the 'time' dimension. + angle : int + The power of the general unit in the 'angle' dimension. """ return self._dimensions[6] diff --git a/python/BioSimSpace/Types/_length.py b/python/BioSimSpace/Types/_length.py index 5eb10fb07..67f163af8 100644 --- a/python/BioSimSpace/Types/_length.py +++ b/python/BioSimSpace/Types/_length.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -87,9 +87,8 @@ class Length(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 1, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -195,29 +194,6 @@ def __rmul__(self, other): # Multiplication is commutative: a*b = b*a return self.__mul__(other) - def __pow__(self, other): - """Power operator.""" - - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - - # No change. - if other == 1: - return self - - # Area. - if other == 2: - mag = self.angstroms().value() ** 2 - return _Area(mag, "A2") - - # Volume. - if other == 3: - mag = self.angstroms().value() ** 3 - return _Volume(mag, "A3") - - else: - return super().__pow__(other) - def meters(self): """ Return the length in meters. @@ -362,7 +338,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -376,13 +353,13 @@ def _validate_unit(self, unit): unit = "ANGS" + unit[3:] # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_pressure.py b/python/BioSimSpace/Types/_pressure.py index 699d9d5f2..fbe6da782 100644 --- a/python/BioSimSpace/Types/_pressure.py +++ b/python/BioSimSpace/Types/_pressure.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -55,9 +55,8 @@ class Pressure(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ATMOSPHERE" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, -1, 1, 0, 0, -2) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -177,7 +176,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -196,11 +196,11 @@ def _validate_unit(self, unit): unit = unit.replace("S", "") # Check that the unit is supported. - if unit in self._abbreviations: - return self._abbreviations[unit] + if unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_temperature.py b/python/BioSimSpace/Types/_temperature.py index f97d2b956..a7010dd7f 100644 --- a/python/BioSimSpace/Types/_temperature.py +++ b/python/BioSimSpace/Types/_temperature.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -60,9 +60,8 @@ class Temperature(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "KELVIN" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 1, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -392,7 +391,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -405,16 +405,16 @@ def _validate_unit(self, unit): unit = unit.replace("DEG", "") # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] elif len(unit) == 0: raise ValueError(f"Unit is not given. You must supply the unit.") else: raise ValueError( "Unsupported unit '%s'. Supported units are: '%s'" - % (unit, list(self._supported_units.keys())) + % (unit, list(cls._supported_units.keys())) ) def _to_sire_unit(self): @@ -444,13 +444,13 @@ def _from_sire_unit(cls, sire_unit): if isinstance(sire_unit, _SireUnits.GeneralUnit): # Create a mask for the dimensions of the object. dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), + sire_unit.LENGTH(), sire_unit.TIME(), + sire_unit.CHARGE(), + sire_unit.TEMPERATURE(), + sire_unit.QUANTITY(), + sire_unit.ANGLE(), ) # Make sure the dimensions match. diff --git a/python/BioSimSpace/Types/_time.py b/python/BioSimSpace/Types/_time.py index 19fc10401..0f8dda977 100644 --- a/python/BioSimSpace/Types/_time.py +++ b/python/BioSimSpace/Types/_time.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -96,9 +96,8 @@ class Time(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "NANOSECOND" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 0, 0, 0, 0, 1) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -337,24 +336,25 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. unit = unit.replace(" ", "").upper() # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit[:-1] in self._supported_units: + elif unit[:-1] in cls._supported_units: return unit[:-1] - elif unit in self._abbreviations: - return self._abbreviations[unit] - elif unit[:-1] in self._abbreviations: - return self._abbreviations[unit[:-1]] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] + elif unit[:-1] in cls._abbreviations: + return cls._abbreviations[unit[:-1]] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/Types/_type.py b/python/BioSimSpace/Types/_type.py index f01635631..75d7b7e0c 100644 --- a/python/BioSimSpace/Types/_type.py +++ b/python/BioSimSpace/Types/_type.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -103,7 +103,7 @@ def __init__(self, *args): self._value = temp._value self._unit = temp._unit - # The user has passed a string representation of the temperature. + # The user has passed a string representation of the type. elif isinstance(args[0], str): # Convert the string to an object of this type. obj = self._from_string(args[0]) @@ -168,12 +168,22 @@ def __add__(self, other): temp = self._from_string(other) return self + temp + # Addition of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for +: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __radd__(self, other): + """Addition operator.""" + + # Addition is commutative: a+b = b+a + return self.__add__(other) + def __sub__(self, other): """Subtraction operator.""" @@ -185,22 +195,32 @@ def __sub__(self, other): # Return a new object of the same type with the original unit. return self._to_default_unit(val)._convert_to(self._unit) - # Addition of a different type with the same dimensions. + # Subtraction of a different type with the same dimensions. elif isinstance(other, Type) and self._dimensions == other.dimensions: # Negate other and add. return -other + self - # Addition of a string. + # Subtraction of a string. elif isinstance(other, str): temp = self._from_string(other) return self - temp + # Subtraction of a zero-valued integer or float. + elif isinstance(other, (int, float)) and other == 0: + return self + else: raise TypeError( "unsupported operand type(s) for -: '%s' and '%s'" % (self.__class__.__qualname__, other.__class__.__qualname__) ) + def __rsub__(self, other): + """Subtraction operator.""" + + # Subtraction is not commutative: a-b != b-a + return -self.__sub__(other) + def __mul__(self, other): """Multiplication operator.""" @@ -244,19 +264,9 @@ def __rmul__(self, other): def __pow__(self, other): """Power operator.""" - if not isinstance(other, int): - raise ValueError("We can only raise to the power of integer values.") - from ._general_unit import GeneralUnit as _GeneralUnit - default_unit = self._to_default_unit() - mag = default_unit.value() ** other - unit = default_unit.unit().lower() - pow_to_mul = "*".join(abs(other) * [unit]) - if other > 0: - return _GeneralUnit(f"{mag}*{pow_to_mul}") - else: - return _GeneralUnit(f"{mag}/({pow_to_mul})") + return _GeneralUnit(self._to_sire_unit(), no_cast=True) ** other def __truediv__(self, other): """Division operator.""" @@ -486,99 +496,99 @@ def dimensions(cls): containing the power in each dimension. Returns : (int, int, int, int, int, int) - The power in each dimension: 'angle', 'charge', 'length', - 'mass', 'quantity', 'temperature', and 'time'. + The power in each dimension: 'mass', 'length', 'temperature', + 'charge', 'time', 'quantity', and 'angle'. """ return cls._dimensions @classmethod - def angle(cls): + def mass(cls): """ - Return the power in the 'angle' dimension. + Return the power in the 'mass' dimension. Returns ------- - angle : int - The power in the 'angle' dimension. + mass : int + The power in the 'mass' dimension. """ return cls._dimensions[0] @classmethod - def charge(cls): + def length(cls): """ - Return the power in the 'charge' dimension. + Return the power in the 'length' dimension. Returns ------- - charge : int - The power in the 'charge' dimension. + length : int + The power in the 'length' dimension. """ return cls._dimensions[1] @classmethod - def length(cls): + def time(cls): """ - Return the power in the 'length' dimension. + Return the power in the 'time' dimension. Returns ------- - length : int - The power in the 'length' dimension. + time : int + The power the 'time' dimension. """ return cls._dimensions[2] @classmethod - def mass(cls): + def charge(cls): """ - Return the power in the 'mass' dimension. + Return the power in the 'charge' dimension. Returns ------- - mass : int - The power in the 'mass' dimension. + charge : int + The power in the 'charge' dimension. """ return cls._dimensions[3] @classmethod - def quantity(cls): + def temperature(cls): """ - Return the power in the 'quantity' dimension. + Return the power in the 'temperature' dimension. Returns ------- - quantity : int - The power in the 'quantity' dimension. + temperature : int + The power in the 'temperature' dimension. """ return cls._dimensions[4] @classmethod - def temperature(cls): + def quantity(cls): """ - Return the power in the 'temperature' dimension. + Return the power in the 'quantity' dimension. Returns ------- - temperature : int - The power in the 'temperature' dimension. + quantity : int + The power in the 'quantity' dimension. """ return cls._dimensions[5] @classmethod - def time(cls): + def angle(cls): """ - Return the power in the 'time' dimension. + Return the power in the 'angle' dimension. Returns ------- - time : int - The power the 'time' dimension. + angle : int + The power in the 'angle' dimension. """ return cls._dimensions[6] @@ -662,15 +672,7 @@ def _from_sire_unit(cls, sire_unit): raise TypeError("'sire_unit' must be of type 'sire.units.GeneralUnit'") # Create a mask for the dimensions of the object. - dimensions = ( - sire_unit.ANGLE(), - sire_unit.CHARGE(), - sire_unit.LENGTH(), - sire_unit.MASS(), - sire_unit.QUANTITY(), - sire_unit.TEMPERATURE(), - sire_unit.TIME(), - ) + dimensions = tuple(sire_unit.dimensions()) # Make sure that this isn't zero. if hasattr(sire_unit, "is_zero"): diff --git a/python/BioSimSpace/Types/_volume.py b/python/BioSimSpace/Types/_volume.py index 4b255e01a..4dad85642 100644 --- a/python/BioSimSpace/Types/_volume.py +++ b/python/BioSimSpace/Types/_volume.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -72,9 +72,8 @@ class Volume(_Type): # Null type unit for avoiding issue printing configargparse help. _default_unit = "ANGSTROM3" - # The dimension mask: - # Angle, Charge, Length, Mass, Quantity, Temperature, Time - _dimensions = (0, 0, 3, 0, 0, 0, 0) + # The dimension mask. + _dimensions = tuple(list(_supported_units.values())[0].dimensions()) def __init__(self, *args): """ @@ -287,7 +286,8 @@ def _convert_to(self, unit): "Supported units are: '%s'" % list(self._supported_units.keys()) ) - def _validate_unit(self, unit): + @classmethod + def _validate_unit(cls, unit): """Validate that the unit are supported.""" # Strip whitespace and convert to upper case. @@ -317,13 +317,13 @@ def _validate_unit(self, unit): unit = unit[0:index] + unit[index + 1 :] + "3" # Check that the unit is supported. - if unit in self._supported_units: + if unit in cls._supported_units: return unit - elif unit in self._abbreviations: - return self._abbreviations[unit] + elif unit in cls._abbreviations: + return cls._abbreviations[unit] else: raise ValueError( - "Supported units are: '%s'" % list(self._supported_units.keys()) + "Supported units are: '%s'" % list(cls._supported_units.keys()) ) @staticmethod diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index ddd51ed41..2a93e52a8 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -224,9 +224,9 @@ def createConfig( ] restraint_mask = "@" + ",".join(restraint_atom_names) elif restraint == "heavy": - restraint_mask = "!:WAT & !@H=" + restraint_mask = "!:WAT & !@%NA,CL & !@H=" elif restraint == "all": - restraint_mask = "!:WAT" + restraint_mask = "!:WAT & !@%NA,CL" # We can't do anything about a custom restraint, since we don't # know anything about the atoms. diff --git a/python/BioSimSpace/_SireWrappers/_molecule.py b/python/BioSimSpace/_SireWrappers/_molecule.py index 9a3a3d70e..089c88146 100644 --- a/python/BioSimSpace/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/_SireWrappers/_molecule.py @@ -1,7 +1,7 @@ ###################################################################### # BioSimSpace: Making biomolecular simulation a breeze! # -# Copyright: 2017-2023 +# Copyright: 2017-2024 # # Authors: Lester Hedges # @@ -746,9 +746,8 @@ def makeCompatibleWith( # Have we matched all of the atoms? if len(matches) < num_atoms0: - # Atom names might have changed. Try to match by residue index - # and coordinates. - matcher = _SireMol.ResIdxAtomCoordMatcher() + # Atom names or order might have changed. Try to match by coordinates. + matcher = _SireMol.AtomCoordMatcher() matches = matcher.match(mol0, mol1) # We need to rename the atoms. @@ -948,9 +947,6 @@ def makeCompatibleWith( # Tally counter for the total number of matches. num_matches = 0 - # Initialise the offset. - offset = 0 - # Get the molecule numbers in the system. mol_nums = mol1.molNums() @@ -960,16 +956,13 @@ def makeCompatibleWith( mol = mol1[num] # Initialise the matcher. - matcher = _SireMol.ResIdxAtomCoordMatcher(_SireMol.ResIdx(offset)) + matcher = _SireMol.AtomCoordMatcher() # Get the matches for this molecule and append to the list. match = matcher.match(mol0, mol) matches.append(match) num_matches += len(match) - # Increment the offset. - offset += mol.nResidues() - # Have we matched all of the atoms? if num_matches < num_atoms0: raise _IncompatibleError("Failed to match all atoms!") diff --git a/recipes/biosimspace/template.yaml b/recipes/biosimspace/template.yaml index 50b670ded..efcb2c141 100644 --- a/recipes/biosimspace/template.yaml +++ b/recipes/biosimspace/template.yaml @@ -26,18 +26,19 @@ test: - SIRE_DONT_PHONEHOME - SIRE_SILENT_PHONEHOME requires: - - pytest - - black 23 # [linux and x86_64 and py==39] - - pytest-black # [linux and x86_64 and py==39] + - pytest <8 + - black 23 # [linux and x86_64 and py==311] + - pytest-black # [linux and x86_64 and py==311] - ambertools # [linux and x86_64] - gromacs # [linux and x86_64] + - requests imports: - BioSimSpace source_files: - - python/BioSimSpace # [linux and x86_64 and py==39] + - python/BioSimSpace # [linux and x86_64 and py==311] - tests commands: - - pytest -vvv --color=yes --black python/BioSimSpace # [linux and x86_64 and py==39] + - pytest -vvv --color=yes --black python/BioSimSpace # [linux and x86_64 and py==311] - pytest -vvv --color=yes --import-mode=importlib tests about: diff --git a/requirements.txt b/requirements.txt index f7bb52604..79c862907 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # BioSimSpace runtime requirements. # main -sire~=2023.5.0 +sire~=2023.5.2 # devel #sire==2023.5.0.dev diff --git a/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py b/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py index c7489f8d2..b34496bb0 100644 --- a/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py +++ b/tests/Sandpit/Exscientia/FreeEnergy/test_restraint_search.py @@ -291,7 +291,15 @@ def test_dict_mdr(self, multiple_distance_restraint): assert restr_dict["permanent_distance_restraint"][ "r0" ].value() == pytest.approx(8.9019, abs=1e-4) - assert restr_dict["permanent_distance_restraint"]["kr"].unit() == "M Q-1 T-2" + assert restr_dict["permanent_distance_restraint"]["kr"].dimensions() == ( + 1, + 0, + -2, + 0, + 0, + -1, + 0, + ) assert restr_dict["permanent_distance_restraint"]["kr"].value() == 40.0 assert restr_dict["permanent_distance_restraint"]["r_fb"].unit() == "ANGSTROM" assert restr_dict["permanent_distance_restraint"][ diff --git a/tests/Sandpit/Exscientia/Process/test_position_restraint.py b/tests/Sandpit/Exscientia/Process/test_position_restraint.py index 7c16427c1..f67cb267e 100644 --- a/tests/Sandpit/Exscientia/Process/test_position_restraint.py +++ b/tests/Sandpit/Exscientia/Process/test_position_restraint.py @@ -197,6 +197,7 @@ def test_gromacs(alchemical_ion_system, restraint, alchemical_ion_system_psores) protocol, name="test", reference_system=alchemical_ion_system_psores, + ignore_warnings=True, ) # Test the position restraint for protein center diff --git a/tests/Sandpit/Exscientia/Types/test_general_unit.py b/tests/Sandpit/Exscientia/Types/test_general_unit.py index 3862c2a47..efc3cd4e1 100644 --- a/tests/Sandpit/Exscientia/Types/test_general_unit.py +++ b/tests/Sandpit/Exscientia/Types/test_general_unit.py @@ -3,15 +3,26 @@ import BioSimSpace.Sandpit.Exscientia.Types as Types import BioSimSpace.Sandpit.Exscientia.Units as Units +import sire as sr + @pytest.mark.parametrize( "string, dimensions", [ - ("kilo Cal oriEs per Mole / angstrom **2", (0, 0, 0, 1, -1, 0, -2)), - ("k Cal_per _mOl / nm^2", (0, 0, 0, 1, -1, 0, -2)), - ("kj p eR moles / pico METERs2", (0, 0, 0, 1, -1, 0, -2)), - ("coul oMbs / secs * ATm os phereS", (0, 1, -1, 1, 0, 0, -3)), - ("pm**3 * rads * de grEE", (2, 0, 3, 0, 0, 0, 0)), + ( + "kilo Cal oriEs per Mole / angstrom **2", + tuple(sr.u("kcal_per_mol / angstrom**2").dimensions()), + ), + ("k Cal_per _mOl / nm^2", tuple(sr.u("kcal_per_mol / nm**2").dimensions())), + ( + "kj p eR moles / pico METERs2", + tuple(sr.u("kJ_per_mol / pm**2").dimensions()), + ), + ( + "coul oMbs / secs * ATm os phereS", + tuple(sr.u("coulombs / second / atm").dimensions()), + ), + ("pm**3 * rads * de grEE", tuple(sr.u("pm**3 * rad * degree").dimensions())), ], ) def test_supported_units(string, dimensions): @@ -140,6 +151,61 @@ def test_neg_pow(unit_type): assert d1 == -d0 +def test_frac_pow(): + """Test that unit-based types can be raised to fractional powers.""" + + # Create a base unit type. + unit_type = 2 * Units.Length.angstrom + + # Store the original value and dimensions. + value = unit_type.value() + dimensions = unit_type.dimensions() + + # Square the type. + unit_type = unit_type**2 + + # Assert that we can't take the cube root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 3) + + # Now take the square root. + unit_type = unit_type ** (1 / 2) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Cube the type. + unit_type = unit_type**3 + + # Assert that we can't take the square root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 2) + + # Now take the cube root. + unit_type = unit_type ** (1 / 3) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Square the type again. + unit_type = unit_type**2 + + # Now take the negative square root. + unit_type = unit_type ** (-1 / 2) + + # The value should be inverted. + assert unit_type.value() == 1 / value + + # The dimensions should be negated. + assert unit_type.dimensions() == tuple(-d for d in dimensions) + + @pytest.mark.parametrize( "string", [ diff --git a/tests/Types/test_general_unit.py b/tests/Types/test_general_unit.py index d97acaf36..ec630d060 100644 --- a/tests/Types/test_general_unit.py +++ b/tests/Types/test_general_unit.py @@ -3,15 +3,26 @@ import BioSimSpace.Types as Types import BioSimSpace.Units as Units +import sire as sr + @pytest.mark.parametrize( "string, dimensions", [ - ("kilo Cal oriEs per Mole / angstrom **2", (0, 0, 0, 1, -1, 0, -2)), - ("k Cal_per _mOl / nm^2", (0, 0, 0, 1, -1, 0, -2)), - ("kj p eR moles / pico METERs2", (0, 0, 0, 1, -1, 0, -2)), - ("coul oMbs / secs * ATm os phereS", (0, 1, -1, 1, 0, 0, -3)), - ("pm**3 * rads * de grEE", (2, 0, 3, 0, 0, 0, 0)), + ( + "kilo Cal oriEs per Mole / angstrom **2", + tuple(sr.u("kcal_per_mol / angstrom**2").dimensions()), + ), + ("k Cal_per _mOl / nm^2", tuple(sr.u("kcal_per_mol / nm**2").dimensions())), + ( + "kj p eR moles / pico METERs2", + tuple(sr.u("kJ_per_mol / pm**2").dimensions()), + ), + ( + "coul oMbs / secs * ATm os phereS", + tuple(sr.u("coulombs / second / atm").dimensions()), + ), + ("pm**3 * rads * de grEE", tuple(sr.u("pm**3 * rad * degree").dimensions())), ], ) def test_supported_units(string, dimensions): @@ -140,6 +151,61 @@ def test_neg_pow(unit_type): assert d1 == -d0 +def test_frac_pow(): + """Test that unit-based types can be raised to fractional powers.""" + + # Create a base unit type. + unit_type = 2 * Units.Length.angstrom + + # Store the original value and dimensions. + value = unit_type.value() + dimensions = unit_type.dimensions() + + # Square the type. + unit_type = unit_type**2 + + # Assert that we can't take the cube root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 3) + + # Now take the square root. + unit_type = unit_type ** (1 / 2) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Cube the type. + unit_type = unit_type**3 + + # Assert that we can't take the square root. + with pytest.raises(ValueError): + unit_type = unit_type ** (1 / 2) + + # Now take the cube root. + unit_type = unit_type ** (1 / 3) + + # The value should be the same. + assert unit_type.value() == value + + # The dimensions should be the same. + assert unit_type.dimensions() == dimensions + + # Square the type again. + unit_type = unit_type**2 + + # Now take the negative square root. + unit_type = unit_type ** (-1 / 2) + + # The value should be inverted. + assert unit_type.value() == 1 / value + + # The dimensions should be negated. + assert unit_type.dimensions() == tuple(-d for d in dimensions) + + @pytest.mark.parametrize( "string", [ From 3b52d29f73bb425b034898e6d8d2a7f5ddc36218 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 21 Mar 2024 13:56:35 +0000 Subject: [PATCH 054/121] Use Langevin integrator for free-energy simulations. --- python/BioSimSpace/_Config/_gromacs.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/python/BioSimSpace/_Config/_gromacs.py b/python/BioSimSpace/_Config/_gromacs.py index 23c50e613..010985613 100644 --- a/python/BioSimSpace/_Config/_gromacs.py +++ b/python/BioSimSpace/_Config/_gromacs.py @@ -207,10 +207,14 @@ def createConfig(self, version=None, extra_options={}, extra_lines=[]): # Temperature control. if not isinstance(self._protocol, _Protocol.Minimisation): - # Leap-frog molecular dynamics. - protocol_dict["integrator"] = "md" - # Temperature coupling using velocity rescaling with a stochastic term. - protocol_dict["tcoupl"] = "v-rescale" + if isinstance(self._protocol, _FreeEnergyMixin): + # Langevin dynamics. + protocol_dict["integrator"] = "sd" + else: + # Leap-frog molecular dynamics. + protocol_dict["integrator"] = "md" + # Temperature coupling using velocity rescaling with a stochastic term. + protocol_dict["tcoupl"] = "v-rescale" # A single temperature group for the entire system. protocol_dict["tc-grps"] = "system" # Thermostat coupling frequency (ps). From 68bcc2c43af6ed15ad2a9b8a787e70eac24adc24 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 22 Mar 2024 12:31:18 +0000 Subject: [PATCH 055/121] Pass explicit_dummies kwargs through to _generate_amber_fep_masks. --- python/BioSimSpace/_Config/_amber.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index 5b81d4b40..2dd446749 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -367,7 +367,9 @@ def createConfig( # Atom masks. protocol_dict = { **protocol_dict, - **self._generate_amber_fep_masks(timestep), + **self._generate_amber_fep_masks( + timestep, explicit_dummies=explicit_dummies + ), } # Put everything together in a line-by-line format. From 6da2b9d407cc866ed0274b06fe0c4518e49a0199 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 22 Mar 2024 13:15:54 +0000 Subject: [PATCH 056/121] Remove redundant openff.Topology import. --- python/BioSimSpace/Parameters/_Protocol/_openforcefield.py | 2 -- .../Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py index 2f6358893..0367b79b0 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py @@ -76,12 +76,10 @@ if _have_imported(_openff): from openff.interchange import Interchange as _Interchange from openff.toolkit.topology import Molecule as _OpenFFMolecule - from openff.toolkit.topology import Topology as _OpenFFTopology from openff.toolkit.typing.engines.smirnoff import ForceField as _Forcefield else: _Interchange = _openff _OpenFFMolecule = _openff - _OpenFFTopology = _openff _Forcefield = _openff # Reset stderr. diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py index 2f6358893..0367b79b0 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py @@ -76,12 +76,10 @@ if _have_imported(_openff): from openff.interchange import Interchange as _Interchange from openff.toolkit.topology import Molecule as _OpenFFMolecule - from openff.toolkit.topology import Topology as _OpenFFTopology from openff.toolkit.typing.engines.smirnoff import ForceField as _Forcefield else: _Interchange = _openff _OpenFFMolecule = _openff - _OpenFFTopology = _openff _Forcefield = _openff # Reset stderr. From c10d28b9aa56f501a8c0ade2628994237625b72c Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 22 Mar 2024 13:44:31 +0000 Subject: [PATCH 057/121] Remove redundant openff.Topology import. --- python/BioSimSpace/Parameters/_Protocol/_openforcefield.py | 2 -- .../Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py index 2f6358893..0367b79b0 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py @@ -76,12 +76,10 @@ if _have_imported(_openff): from openff.interchange import Interchange as _Interchange from openff.toolkit.topology import Molecule as _OpenFFMolecule - from openff.toolkit.topology import Topology as _OpenFFTopology from openff.toolkit.typing.engines.smirnoff import ForceField as _Forcefield else: _Interchange = _openff _OpenFFMolecule = _openff - _OpenFFTopology = _openff _Forcefield = _openff # Reset stderr. diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py index 2f6358893..0367b79b0 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py @@ -76,12 +76,10 @@ if _have_imported(_openff): from openff.interchange import Interchange as _Interchange from openff.toolkit.topology import Molecule as _OpenFFMolecule - from openff.toolkit.topology import Topology as _OpenFFTopology from openff.toolkit.typing.engines.smirnoff import ForceField as _Forcefield else: _Interchange = _openff _OpenFFMolecule = _openff - _OpenFFTopology = _openff _Forcefield = _openff # Reset stderr. From 40ecb824fafe7deeb64c5b3fb2c9e9103db7ff10 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 22 Mar 2024 14:03:23 +0000 Subject: [PATCH 058/121] Add support for using NAGL to generate AM1BCC charges. --- .../Parameters/_Protocol/_openforcefield.py | 42 ++++++++++++++++++- .../Parameters/_Protocol/_openforcefield.py | 42 ++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py index 0367b79b0..5cb2d6c8e 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py @@ -77,6 +77,27 @@ from openff.interchange import Interchange as _Interchange from openff.toolkit.topology import Molecule as _OpenFFMolecule from openff.toolkit.typing.engines.smirnoff import ForceField as _Forcefield + + try: + from openff.toolkit.utils.nagl_wrapper import ( + NAGLToolkitWrapper as _NAGLToolkitWrapper, + ) + + _has_nagl = _NAGLToolkitWrapper.is_available() + from openff.nagl_models import get_models_by_type as _get_models_by_type + + _models = _get_models_by_type("am1bcc") + try: + # Find the most recent AM1-BCC release candidate. + _nagl = _NAGLToolkitWrapper() + _nagl_model = sorted( + [str(model) for model in _models if "rc" in str(model)], reverse=True + )[0] + except: + _has_nagl = False + del _models + except: + _has_nagl = False else: _Interchange = _openff _OpenFFMolecule = _openff @@ -289,6 +310,23 @@ def run(self, molecule, work_dir=None, queue=None): else: raise _ThirdPartyError(msg) from None + # Apply AM1-BCC charges using NAGL. + if _has_nagl: + try: + _nagl.assign_partial_charges( + off_molecule, partial_charge_method=_nagl_model + ) + except Exception as e: + msg = "Failed to assign AM1-BCC charges using NAGL." + if _isVerbose(): + msg += ": " + getattr(e, "message", repr(e)) + raise _ThirdPartyError(msg) from e + else: + raise _ThirdPartyError(msg) from None + charge_from_molecules = [off_molecule] + else: + charge_from_molecules = None + # Extract the molecular topology. try: off_topology = off_molecule.to_topology() @@ -315,7 +353,9 @@ def run(self, molecule, work_dir=None, queue=None): # Create an Interchange object. try: interchange = _Interchange.from_smirnoff( - force_field=forcefield, topology=off_topology + force_field=forcefield, + topology=off_topology, + charge_from_molecules=charge_from_molecules, ) except Exception as e: msg = "Unable to create OpenFF Interchange object!" diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py index 0367b79b0..5cb2d6c8e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py @@ -77,6 +77,27 @@ from openff.interchange import Interchange as _Interchange from openff.toolkit.topology import Molecule as _OpenFFMolecule from openff.toolkit.typing.engines.smirnoff import ForceField as _Forcefield + + try: + from openff.toolkit.utils.nagl_wrapper import ( + NAGLToolkitWrapper as _NAGLToolkitWrapper, + ) + + _has_nagl = _NAGLToolkitWrapper.is_available() + from openff.nagl_models import get_models_by_type as _get_models_by_type + + _models = _get_models_by_type("am1bcc") + try: + # Find the most recent AM1-BCC release candidate. + _nagl = _NAGLToolkitWrapper() + _nagl_model = sorted( + [str(model) for model in _models if "rc" in str(model)], reverse=True + )[0] + except: + _has_nagl = False + del _models + except: + _has_nagl = False else: _Interchange = _openff _OpenFFMolecule = _openff @@ -289,6 +310,23 @@ def run(self, molecule, work_dir=None, queue=None): else: raise _ThirdPartyError(msg) from None + # Apply AM1-BCC charges using NAGL. + if _has_nagl: + try: + _nagl.assign_partial_charges( + off_molecule, partial_charge_method=_nagl_model + ) + except Exception as e: + msg = "Failed to assign AM1-BCC charges using NAGL." + if _isVerbose(): + msg += ": " + getattr(e, "message", repr(e)) + raise _ThirdPartyError(msg) from e + else: + raise _ThirdPartyError(msg) from None + charge_from_molecules = [off_molecule] + else: + charge_from_molecules = None + # Extract the molecular topology. try: off_topology = off_molecule.to_topology() @@ -315,7 +353,9 @@ def run(self, molecule, work_dir=None, queue=None): # Create an Interchange object. try: interchange = _Interchange.from_smirnoff( - force_field=forcefield, topology=off_topology + force_field=forcefield, + topology=off_topology, + charge_from_molecules=charge_from_molecules, ) except Exception as e: msg = "Unable to create OpenFF Interchange object!" From 9355e30fa726c860796ad44a77836e8adb25ee54 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 25 Mar 2024 09:19:32 +0000 Subject: [PATCH 059/121] Add default _has_nagl flag. --- python/BioSimSpace/Parameters/_Protocol/_openforcefield.py | 3 +++ .../Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py index 5cb2d6c8e..7c3aed05a 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py @@ -73,6 +73,9 @@ _openff = _try_import("openff") +# Initialise the NAGL support flag. +_has_nagl = False + if _have_imported(_openff): from openff.interchange import Interchange as _Interchange from openff.toolkit.topology import Molecule as _OpenFFMolecule diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py index 5cb2d6c8e..7c3aed05a 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py @@ -73,6 +73,9 @@ _openff = _try_import("openff") +# Initialise the NAGL support flag. +_has_nagl = False + if _have_imported(_openff): from openff.interchange import Interchange as _Interchange from openff.toolkit.topology import Molecule as _OpenFFMolecule From 0a3bd903a1f035d6a8c5820f03d98e198400c964 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 25 Mar 2024 09:49:12 +0000 Subject: [PATCH 060/121] Add support for clearing and disabling the file cache. [closes #265] --- python/BioSimSpace/IO/__init__.py | 4 ++ python/BioSimSpace/IO/_file_cache.py | 45 +++++++++++++++++-- python/BioSimSpace/IO/_io.py | 29 +++++++----- .../Sandpit/Exscientia/IO/__init__.py | 6 ++- .../Sandpit/Exscientia/IO/_file_cache.py | 45 +++++++++++++++++-- .../BioSimSpace/Sandpit/Exscientia/IO/_io.py | 31 ++++++++----- tests/IO/test_file_cache.py | 22 ++++++++- .../Sandpit/Exscientia/IO/test_file_cache.py | 22 ++++++++- 8 files changed, 169 insertions(+), 35 deletions(-) diff --git a/python/BioSimSpace/IO/__init__.py b/python/BioSimSpace/IO/__init__.py index 405005f35..95d60126d 100644 --- a/python/BioSimSpace/IO/__init__.py +++ b/python/BioSimSpace/IO/__init__.py @@ -28,6 +28,9 @@ .. autosummary:: :toctree: generated/ + clearCache + disableCache + enableCache fileFormats formatInfo readMolecules @@ -38,3 +41,4 @@ """ from ._io import * +from ._file_cache import * diff --git a/python/BioSimSpace/IO/_file_cache.py b/python/BioSimSpace/IO/_file_cache.py index 0b55aef9a..6f5b2d691 100644 --- a/python/BioSimSpace/IO/_file_cache.py +++ b/python/BioSimSpace/IO/_file_cache.py @@ -24,7 +24,7 @@ __author__ = "Lester Hedges" __email__ = "lester.hedges@gmail.com" -__all__ = ["check_cache", "update_cache"] +__all__ = ["clearCache", "disableCache", "enableCache"] import collections as _collections import hashlib as _hashlib @@ -80,8 +80,43 @@ def __delitem__(self, key): # to the same format, allowing us to re-use the existing file. _cache = _FixedSizeOrderedDict() +# Whether to use the cache. +_use_cache = True -def check_cache( + +def clearCache(): + """ + Clear the file cache. + """ + global _cache + _cache = _FixedSizeOrderedDict() + + +def disableCache(): + """ + Disable the file cache. + """ + global _use_cache + _use_cache = False + + +def enableCache(): + """ + Enable the file cache. + """ + global _use_cache + _use_cache = True + + +def _cache_active(): + """ + Internal helper function to check whether the cache is active. + """ + global _use_cache + return _use_cache + + +def _check_cache( system, format, filebase, @@ -157,6 +192,8 @@ def check_cache( if not isinstance(skip_water, bool): raise TypeError("'skip_water' must be of type 'bool'.") + global _cache + # Create the key. key = ( system._sire_object.uid().toString(), @@ -221,7 +258,7 @@ def check_cache( return ext -def update_cache( +def _update_cache( system, format, path, @@ -284,6 +321,8 @@ def update_cache( if not isinstance(skip_water, bool): raise TypeError("'skip_water' must be of type 'bool'.") + global _cache + # Convert to an absolute path. path = _os.path.abspath(path) diff --git a/python/BioSimSpace/IO/_io.py b/python/BioSimSpace/IO/_io.py index 3fcf08e23..9aca4a3d4 100644 --- a/python/BioSimSpace/IO/_io.py +++ b/python/BioSimSpace/IO/_io.py @@ -66,8 +66,9 @@ from .._SireWrappers import System as _System from .. import _Utils -from ._file_cache import check_cache as _check_cache -from ._file_cache import update_cache as _update_cache +from ._file_cache import _check_cache +from ._file_cache import _update_cache +from ._file_cache import _cache_active # Context manager for capturing stdout. @@ -741,14 +742,17 @@ def saveMolecules( # Save the system using each file format. for format in formats: # Copy an existing file if it exists in the cache. - ext = _check_cache( - system, - format, - filebase, - match_water=match_water, - property_map=property_map, - **kwargs, - ) + if _cache_active(): + ext = _check_cache( + system, + format, + filebase, + match_water=match_water, + property_map=property_map, + **kwargs, + ) + else: + ext = None if ext: files.append(_os.path.abspath(filebase + ext)) continue @@ -835,7 +839,10 @@ def saveMolecules( files += file # If this is a new file, then add it to the cache. - _update_cache(system, format, file[0], match_water=match_water, **kwargs) + if _cache_active(): + _update_cache( + system, format, file[0], match_water=match_water, **kwargs + ) except Exception as e: msg = "Failed to save system to format: '%s'" % format diff --git a/python/BioSimSpace/Sandpit/Exscientia/IO/__init__.py b/python/BioSimSpace/Sandpit/Exscientia/IO/__init__.py index e83faacca..95d60126d 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/IO/__init__.py +++ b/python/BioSimSpace/Sandpit/Exscientia/IO/__init__.py @@ -28,6 +28,9 @@ .. autosummary:: :toctree: generated/ + clearCache + disableCache + enableCache fileFormats formatInfo readMolecules @@ -37,6 +40,5 @@ savePerturbableSystem """ -from glob import glob - from ._io import * +from ._file_cache import * diff --git a/python/BioSimSpace/Sandpit/Exscientia/IO/_file_cache.py b/python/BioSimSpace/Sandpit/Exscientia/IO/_file_cache.py index 0b55aef9a..6f5b2d691 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/IO/_file_cache.py +++ b/python/BioSimSpace/Sandpit/Exscientia/IO/_file_cache.py @@ -24,7 +24,7 @@ __author__ = "Lester Hedges" __email__ = "lester.hedges@gmail.com" -__all__ = ["check_cache", "update_cache"] +__all__ = ["clearCache", "disableCache", "enableCache"] import collections as _collections import hashlib as _hashlib @@ -80,8 +80,43 @@ def __delitem__(self, key): # to the same format, allowing us to re-use the existing file. _cache = _FixedSizeOrderedDict() +# Whether to use the cache. +_use_cache = True -def check_cache( + +def clearCache(): + """ + Clear the file cache. + """ + global _cache + _cache = _FixedSizeOrderedDict() + + +def disableCache(): + """ + Disable the file cache. + """ + global _use_cache + _use_cache = False + + +def enableCache(): + """ + Enable the file cache. + """ + global _use_cache + _use_cache = True + + +def _cache_active(): + """ + Internal helper function to check whether the cache is active. + """ + global _use_cache + return _use_cache + + +def _check_cache( system, format, filebase, @@ -157,6 +192,8 @@ def check_cache( if not isinstance(skip_water, bool): raise TypeError("'skip_water' must be of type 'bool'.") + global _cache + # Create the key. key = ( system._sire_object.uid().toString(), @@ -221,7 +258,7 @@ def check_cache( return ext -def update_cache( +def _update_cache( system, format, path, @@ -284,6 +321,8 @@ def update_cache( if not isinstance(skip_water, bool): raise TypeError("'skip_water' must be of type 'bool'.") + global _cache + # Convert to an absolute path. path = _os.path.abspath(path) diff --git a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py index 97ac66348..9aca4a3d4 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py +++ b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py @@ -66,8 +66,9 @@ from .._SireWrappers import System as _System from .. import _Utils -from ._file_cache import check_cache as _check_cache -from ._file_cache import update_cache as _update_cache +from ._file_cache import _check_cache +from ._file_cache import _update_cache +from ._file_cache import _cache_active # Context manager for capturing stdout. @@ -741,14 +742,17 @@ def saveMolecules( # Save the system using each file format. for format in formats: # Copy an existing file if it exists in the cache. - ext = _check_cache( - system, - format, - filebase, - match_water=match_water, - property_map=property_map, - **kwargs, - ) + if _cache_active(): + ext = _check_cache( + system, + format, + filebase, + match_water=match_water, + property_map=property_map, + **kwargs, + ) + else: + ext = None if ext: files.append(_os.path.abspath(filebase + ext)) continue @@ -835,7 +839,10 @@ def saveMolecules( files += file # If this is a new file, then add it to the cache. - _update_cache(system, format, file[0], match_water=match_water, **kwargs) + if _cache_active(): + _update_cache( + system, format, file[0], match_water=match_water, **kwargs + ) except Exception as e: msg = "Failed to save system to format: '%s'" % format @@ -1162,7 +1169,7 @@ def readPerturbableSystem(top0, coords0, top1, coords1, property_map={}): prop = property_map.get("time", "time") time = system0._sire_object.property(prop) system0._sire_object.removeSharedProperty(prop) - system0._sire_object.setPropery(prop, time) + system0._sire_object.setProperty(prop, time) except: pass diff --git a/tests/IO/test_file_cache.py b/tests/IO/test_file_cache.py index 9d3866c99..a431cfc03 100644 --- a/tests/IO/test_file_cache.py +++ b/tests/IO/test_file_cache.py @@ -15,7 +15,7 @@ def test_file_cache(): """ # Clear the file cache. - BSS.IO._file_cache._cache = BSS.IO._file_cache._FixedSizeOrderedDict() + BSS.IO.clearCache() # Load the molecular system. s = BSS.IO.readMolecules(["tests/input/ala.crd", "tests/input/ala.top"]) @@ -82,6 +82,21 @@ def test_file_cache(): # Make sure the number of atoms in the cache was decremented. assert BSS.IO._file_cache._cache._num_atoms == total_atoms - num_atoms + # Clear the file cache. + BSS.IO.clearCache() + + # The cache should now be empty. + assert len(BSS.IO._file_cache._cache) == 0 + + # Disable the cache. + BSS.IO.disableCache() + + # Write to PDB and GroTop format. The PDB from the cache should not be reused. + BSS.IO.saveMolecules(f"{tmp_path}/tmp5", s, ["pdb", "grotop"]) + + # The cache should still be empty. + assert len(BSS.IO._file_cache._cache) == 0 + @pytest.mark.skipif( has_amber is False or has_openff is False, @@ -93,8 +108,11 @@ def test_file_cache_mol_nums(): contain different MolNUms. """ + # Enable the cache. + BSS.IO.enableCache() + # Clear the file cache. - BSS.IO._file_cache._cache = BSS.IO._file_cache._FixedSizeOrderedDict() + BSS.IO.clearCache() # Create an initial system. system = BSS.Parameters.openff_unconstrained_2_0_0("CO").getMolecule().toSystem() diff --git a/tests/Sandpit/Exscientia/IO/test_file_cache.py b/tests/Sandpit/Exscientia/IO/test_file_cache.py index 20ed2f9f3..6e292b66c 100644 --- a/tests/Sandpit/Exscientia/IO/test_file_cache.py +++ b/tests/Sandpit/Exscientia/IO/test_file_cache.py @@ -16,7 +16,7 @@ def test_file_cache(): """ # Clear the file cache. - BSS.IO._file_cache._cache = BSS.IO._file_cache._FixedSizeOrderedDict() + BSS.IO.clearCache() # Load the molecular system. s = BSS.IO.readMolecules([f"{root_fp}/input/ala.crd", f"{root_fp}/input/ala.top"]) @@ -83,6 +83,21 @@ def test_file_cache(): # Make sure the number of atoms in the cache was decremented. assert BSS.IO._file_cache._cache._num_atoms == total_atoms - num_atoms + # Clear the file cache. + BSS.IO.clearCache() + + # The cache should now be empty. + assert len(BSS.IO._file_cache._cache) == 0 + + # Disable the cache. + BSS.IO.disableCache() + + # Write to PDB and GroTop format. The PDB from the cache should not be reused. + BSS.IO.saveMolecules(f"{tmp_path}/tmp5", s, ["pdb", "grotop"]) + + # The cache should still be empty. + assert len(BSS.IO._file_cache._cache) == 0 + @pytest.mark.skipif( has_amber is False or has_openff is False, @@ -94,8 +109,11 @@ def test_file_cache_mol_nums(): contain different MolNUms. """ + # Enable the cache. + BSS.IO.enableCache() + # Clear the file cache. - BSS.IO._file_cache._cache = BSS.IO._file_cache._FixedSizeOrderedDict() + BSS.IO.clearCache() # Create an initial system. system = BSS.Parameters.openff_unconstrained_2_0_0("CO").getMolecule().toSystem() From 0e2bb922ce314536c9efa3126c19edf0b783be1b Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 25 Mar 2024 15:57:01 +0000 Subject: [PATCH 061/121] Allow user to choose to use NAGL to generate AM1-BCC charges. --- .../Parameters/_Protocol/_openforcefield.py | 17 ++++++++++-- python/BioSimSpace/Parameters/_parameters.py | 27 +++++++++++++++++-- .../Parameters/_Protocol/_openforcefield.py | 17 ++++++++++-- .../Exscientia/Parameters/_parameters.py | 27 +++++++++++++++++-- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py index 7c3aed05a..018f43e4e 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py @@ -127,7 +127,9 @@ class OpenForceField(_protocol.Protocol): """A class for handling protocols for Open Force Field models.""" - def __init__(self, forcefield, ensure_compatible=True, property_map={}): + def __init__( + self, forcefield, ensure_compatible=True, use_nagl=True, property_map={} + ): """ Constructor. @@ -145,6 +147,11 @@ def __init__(self, forcefield, ensure_compatible=True, property_map={}): original molecule, e.g. the original atom and residue names will be kept. + use_nagl : bool + Whether to use NAGL to compute AM1-BCC charges. If False, the default + is to use AmberTools via antechamber and sqm. (This option is only + used if NAGL is available.) + property_map : dict A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their @@ -158,6 +165,12 @@ def __init__(self, forcefield, ensure_compatible=True, property_map={}): property_map=property_map, ) + if not isinstance(use_nagl, bool): + raise TypeError("'use_nagl' must be of type 'bool'") + + # Set the NAGL flag. + self._use_nagl = use_nagl + # Set the compatibility flags. self._tleap = False self._pdb2gmx = False @@ -314,7 +327,7 @@ def run(self, molecule, work_dir=None, queue=None): raise _ThirdPartyError(msg) from None # Apply AM1-BCC charges using NAGL. - if _has_nagl: + if _has_nagl and self._use_nagl: try: _nagl.assign_partial_charges( off_molecule, partial_charge_method=_nagl_model diff --git a/python/BioSimSpace/Parameters/_parameters.py b/python/BioSimSpace/Parameters/_parameters.py index 07a49d24b..6e8570bb9 100644 --- a/python/BioSimSpace/Parameters/_parameters.py +++ b/python/BioSimSpace/Parameters/_parameters.py @@ -463,6 +463,7 @@ def _parameterise_openff( forcefield, molecule, ensure_compatible=True, + use_nagl=True, work_dir=None, property_map={}, **kwargs, @@ -489,6 +490,11 @@ def _parameterise_openff( the parameterised molecule will preserve the topology of the original molecule, e.g. the original atom and residue names will be kept. + use_nagl : bool + Whether to use NAGL to compute AM1-BCC charges. If False, the default + is to use AmberTools via antechamber and sqm. (This option is only + used if NAGL is available.) + work_dir : str The working directory for the process. @@ -583,12 +589,21 @@ def _parameterise_openff( if forcefield not in _forcefields_lower: raise ValueError("Supported force fields are: %s" % openForceFields()) + if not isinstance(ensure_compatible, bool): + raise TypeError("'ensure_compatible' must be of type 'bool'.") + + if not isinstance(use_nagl, bool): + raise TypeError("'use_nagl' must be of type 'bool'.") + if not isinstance(property_map, dict): raise TypeError("'property_map' must be of type 'dict'") # Create a default protocol. protocol = _Protocol.OpenForceField( - forcefield, ensure_compatible=ensure_compatible, property_map=property_map + forcefield, + ensure_compatible=ensure_compatible, + use_nagl=use_nagl, + property_map=property_map, ) # Run the parameterisation protocol in the background and return @@ -1079,7 +1094,9 @@ def _function( # it conforms to sensible function naming standards, i.e. "-" and "." # characters replaced by underscores. def _make_openff_function(name): - def _function(molecule, ensure_compatible=True, work_dir=None, property_map={}): + def _function( + molecule, ensure_compatible=True, use_nagl=True, work_dir=None, property_map={} + ): """ Parameterise a molecule using the named force field from the Open Force Field initiative. @@ -1100,6 +1117,11 @@ def _function(molecule, ensure_compatible=True, work_dir=None, property_map={}): molecule will preserve the topology of the original molecule, e.g. the original atom and residue names will be kept. + use_nagl : bool + Whether to use NAGL to compute AM1-BCC charges. If False, the default + is to use AmberTools via antechamber and sqm. (This option is only + used if NAGL is available.) + work_dir : str The working directory for the process. @@ -1118,6 +1140,7 @@ def _function(molecule, ensure_compatible=True, work_dir=None, property_map={}): name, molecule, ensure_compatible=ensure_compatible, + use_nagl=use_nagl, work_dir=work_dir, property_map=property_map, ) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py index 7c3aed05a..018f43e4e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py @@ -127,7 +127,9 @@ class OpenForceField(_protocol.Protocol): """A class for handling protocols for Open Force Field models.""" - def __init__(self, forcefield, ensure_compatible=True, property_map={}): + def __init__( + self, forcefield, ensure_compatible=True, use_nagl=True, property_map={} + ): """ Constructor. @@ -145,6 +147,11 @@ def __init__(self, forcefield, ensure_compatible=True, property_map={}): original molecule, e.g. the original atom and residue names will be kept. + use_nagl : bool + Whether to use NAGL to compute AM1-BCC charges. If False, the default + is to use AmberTools via antechamber and sqm. (This option is only + used if NAGL is available.) + property_map : dict A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their @@ -158,6 +165,12 @@ def __init__(self, forcefield, ensure_compatible=True, property_map={}): property_map=property_map, ) + if not isinstance(use_nagl, bool): + raise TypeError("'use_nagl' must be of type 'bool'") + + # Set the NAGL flag. + self._use_nagl = use_nagl + # Set the compatibility flags. self._tleap = False self._pdb2gmx = False @@ -314,7 +327,7 @@ def run(self, molecule, work_dir=None, queue=None): raise _ThirdPartyError(msg) from None # Apply AM1-BCC charges using NAGL. - if _has_nagl: + if _has_nagl and self._use_nagl: try: _nagl.assign_partial_charges( off_molecule, partial_charge_method=_nagl_model diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_parameters.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_parameters.py index 07a49d24b..6e8570bb9 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_parameters.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_parameters.py @@ -463,6 +463,7 @@ def _parameterise_openff( forcefield, molecule, ensure_compatible=True, + use_nagl=True, work_dir=None, property_map={}, **kwargs, @@ -489,6 +490,11 @@ def _parameterise_openff( the parameterised molecule will preserve the topology of the original molecule, e.g. the original atom and residue names will be kept. + use_nagl : bool + Whether to use NAGL to compute AM1-BCC charges. If False, the default + is to use AmberTools via antechamber and sqm. (This option is only + used if NAGL is available.) + work_dir : str The working directory for the process. @@ -583,12 +589,21 @@ def _parameterise_openff( if forcefield not in _forcefields_lower: raise ValueError("Supported force fields are: %s" % openForceFields()) + if not isinstance(ensure_compatible, bool): + raise TypeError("'ensure_compatible' must be of type 'bool'.") + + if not isinstance(use_nagl, bool): + raise TypeError("'use_nagl' must be of type 'bool'.") + if not isinstance(property_map, dict): raise TypeError("'property_map' must be of type 'dict'") # Create a default protocol. protocol = _Protocol.OpenForceField( - forcefield, ensure_compatible=ensure_compatible, property_map=property_map + forcefield, + ensure_compatible=ensure_compatible, + use_nagl=use_nagl, + property_map=property_map, ) # Run the parameterisation protocol in the background and return @@ -1079,7 +1094,9 @@ def _function( # it conforms to sensible function naming standards, i.e. "-" and "." # characters replaced by underscores. def _make_openff_function(name): - def _function(molecule, ensure_compatible=True, work_dir=None, property_map={}): + def _function( + molecule, ensure_compatible=True, use_nagl=True, work_dir=None, property_map={} + ): """ Parameterise a molecule using the named force field from the Open Force Field initiative. @@ -1100,6 +1117,11 @@ def _function(molecule, ensure_compatible=True, work_dir=None, property_map={}): molecule will preserve the topology of the original molecule, e.g. the original atom and residue names will be kept. + use_nagl : bool + Whether to use NAGL to compute AM1-BCC charges. If False, the default + is to use AmberTools via antechamber and sqm. (This option is only + used if NAGL is available.) + work_dir : str The working directory for the process. @@ -1118,6 +1140,7 @@ def _function(molecule, ensure_compatible=True, work_dir=None, property_map={}): name, molecule, ensure_compatible=ensure_compatible, + use_nagl=use_nagl, work_dir=work_dir, property_map=property_map, ) From af9cced9bfeab433792a9475ca6d20bca64d7b0b Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 25 Mar 2024 16:38:43 +0000 Subject: [PATCH 062/121] Upgrade setup-miniconda action. --- .github/workflows/devel.yaml | 2 +- .github/workflows/main.yaml | 2 +- .github/workflows/pr.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index ce34c30cc..a9640d353 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -36,7 +36,7 @@ jobs: SIRE_DONT_PHONEHOME: 1 SIRE_SILENT_PHONEHOME: 1 steps: - - uses: conda-incubator/setup-miniconda@v2 + - uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 8b36c02b1..0b6dc5d86 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -30,7 +30,7 @@ jobs: SIRE_DONT_PHONEHOME: 1 SIRE_SILENT_PHONEHOME: 1 steps: - - uses: conda-incubator/setup-miniconda@v2 + - uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 5e9268b58..2a7dd5474 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -40,7 +40,7 @@ jobs: SIRE_SILENT_PHONEHOME: 1 REPO: "${{ github.event.pull_request.head.repo.full_name || github.repository }}" steps: - - uses: conda-incubator/setup-miniconda@v2 + - uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true python-version: ${{ matrix.python-version }} From 04b423dfb4d4145b41dbe1f1fe1b3554b802cbbc Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 25 Mar 2024 16:44:04 +0000 Subject: [PATCH 063/121] Manually add boltons. --- .github/workflows/devel.yaml | 2 +- .github/workflows/main.yaml | 2 +- .github/workflows/pr.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index a9640d353..d2a269b50 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -49,7 +49,7 @@ jobs: run: git clone -b devel https://github.com/openbiosim/biosimspace # - name: Setup Conda - run: mamba install -y -c conda-forge boa anaconda-client packaging=21 pip-requirements-parser + run: mamba install -y -c conda-forge boa boltons anaconda-client packaging=21 pip-requirements-parser # - name: Update Conda recipe run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 0b6dc5d86..f9181191e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -43,7 +43,7 @@ jobs: run: git clone -b main https://github.com/openbiosim/biosimspace # - name: Setup Conda - run: mamba install -y -c conda-forge boa anaconda-client packaging=21 pip-requirements-parser + run: mamba install -y -c conda-forge boa boltons anaconda-client packaging=21 pip-requirements-parser # - name: Update Conda recipe run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 2a7dd5474..fd6fbbf29 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -53,7 +53,7 @@ jobs: run: git clone -b ${{ github.head_ref }} --single-branch https://github.com/${{ env.REPO }} biosimspace # - name: Setup Conda - run: mamba install -y -c conda-forge boa anaconda-client packaging=21 pip-requirements-parser + run: mamba install -y -c conda-forge boa boltons anaconda-client packaging=21 pip-requirements-parser # - name: Update Conda recipe run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py From f987c2b7c33fd8481a82462e73f99af75308b222 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 26 Mar 2024 11:59:41 +0000 Subject: [PATCH 064/121] Pin to boa version 0.16. --- .github/workflows/devel.yaml | 2 +- .github/workflows/main.yaml | 2 +- .github/workflows/pr.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index d2a269b50..31869037b 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -49,7 +49,7 @@ jobs: run: git clone -b devel https://github.com/openbiosim/biosimspace # - name: Setup Conda - run: mamba install -y -c conda-forge boa boltons anaconda-client packaging=21 pip-requirements-parser + run: mamba install -y -c conda-forge boa=0.16 anaconda-client packaging=21 pip-requirements-parser # - name: Update Conda recipe run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f9181191e..a5c441797 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -43,7 +43,7 @@ jobs: run: git clone -b main https://github.com/openbiosim/biosimspace # - name: Setup Conda - run: mamba install -y -c conda-forge boa boltons anaconda-client packaging=21 pip-requirements-parser + run: mamba install -y -c conda-forge boa=0.16 anaconda-client packaging=21 pip-requirements-parser # - name: Update Conda recipe run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index fd6fbbf29..3ef002f5e 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -53,7 +53,7 @@ jobs: run: git clone -b ${{ github.head_ref }} --single-branch https://github.com/${{ env.REPO }} biosimspace # - name: Setup Conda - run: mamba install -y -c conda-forge boa boltons anaconda-client packaging=21 pip-requirements-parser + run: mamba install -y -c conda-forge boa=0.16 anaconda-client packaging=21 pip-requirements-parser # - name: Update Conda recipe run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py From 46da6e724d92ac6b96e1336df867c014ad683cbd Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 28 Mar 2024 09:40:02 +0000 Subject: [PATCH 065/121] Glob after converting files to a list. --- python/BioSimSpace/IO/_io.py | 9 ++++++--- python/BioSimSpace/Sandpit/Exscientia/IO/_io.py | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/python/BioSimSpace/IO/_io.py b/python/BioSimSpace/IO/_io.py index 9aca4a3d4..433781188 100644 --- a/python/BioSimSpace/IO/_io.py +++ b/python/BioSimSpace/IO/_io.py @@ -431,11 +431,8 @@ def readMolecules( ) _has_gmx_warned = True - # Glob string to catch wildcards and convert to list. if isinstance(files, str): if not files.startswith(("http", "www")): - files = _glob(files) - else: files = [files] # Check that all arguments are of type 'str'. @@ -450,6 +447,12 @@ def readMolecules( else: raise TypeError("'files' must be of type 'str', or a list of 'str' types.") + # Glob all files to catch wildcards. + new_files = [] + for file in files: + new_files += _glob(file) + files = new_files + # Validate the molecule unwrapping flag. if not isinstance(make_whole, bool): raise TypeError("'make_whole' must be of type 'bool'.") diff --git a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py index 9aca4a3d4..433781188 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py +++ b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py @@ -431,11 +431,8 @@ def readMolecules( ) _has_gmx_warned = True - # Glob string to catch wildcards and convert to list. if isinstance(files, str): if not files.startswith(("http", "www")): - files = _glob(files) - else: files = [files] # Check that all arguments are of type 'str'. @@ -450,6 +447,12 @@ def readMolecules( else: raise TypeError("'files' must be of type 'str', or a list of 'str' types.") + # Glob all files to catch wildcards. + new_files = [] + for file in files: + new_files += _glob(file) + files = new_files + # Validate the molecule unwrapping flag. if not isinstance(make_whole, bool): raise TypeError("'make_whole' must be of type 'bool'.") From 426e30aa9f8c0c57d84cec6e9c069cb1d45b1cb9 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 28 Mar 2024 09:58:54 +0000 Subject: [PATCH 066/121] Add support for adding molecules from a SearchResult. --- .../Exscientia/_SireWrappers/_system.py | 30 +++++++++++++++---- python/BioSimSpace/_SireWrappers/_system.py | 30 +++++++++++++++---- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py index e51f141c7..56bf3aebe 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py @@ -554,12 +554,17 @@ def addMolecules(self, molecules): molecules : :class:`Molecule `, \ :class:`Molecules `, \ [:class:`Molecule `], \ - :class:`System ` - A Molecule, Molecules object, a list of Molecule objects, or a System containing molecules. + :class:`System `, \ + :class:`SearchResult ` + A Molecule, Molecules object, a list of Molecule objects, a System, + or a SearchResult containing molecules. """ - # Whether the molecules are in a Sire container. - is_sire_container = False + from ._search_result import SearchResult as _SearchResult + from sire.legacy.Mol import SelectorMol as _SelectorMol + + # Whether this is a selector mol object. + is_selector_mol = False # Convert tuple to a list. if isinstance(molecules, tuple): @@ -583,6 +588,17 @@ def addMolecules(self, molecules): ): molecules = _Molecules(molecules) + # A SearchResult object. + elif isinstance(molecules, _SearchResult): + if isinstance(molecules._sire_object, _SelectorMol): + is_selector_mol = True + pass + else: + raise ValueError( + "Invalid 'SearchResult' object. Can only add a molecule " + "search, i.e. a wrapped 'sire.legacy.Mol.SelectorMol'." + ) + # Invalid argument. else: raise TypeError( @@ -619,7 +635,11 @@ def addMolecules(self, molecules): ) # Add the molecules to the system. - self._sire_object.add(molecules._sire_object, _SireMol.MGName("all")) + if is_selector_mol: + for mol in molecules: + self._sire_object.add(mol._sire_object, _SireMol.MGName("all")) + else: + self._sire_object.add(molecules._sire_object, _SireMol.MGName("all")) # Reset the index mappings. self._reset_mappings() diff --git a/python/BioSimSpace/_SireWrappers/_system.py b/python/BioSimSpace/_SireWrappers/_system.py index 52ed9c083..380a3b736 100644 --- a/python/BioSimSpace/_SireWrappers/_system.py +++ b/python/BioSimSpace/_SireWrappers/_system.py @@ -554,12 +554,17 @@ def addMolecules(self, molecules): molecules : :class:`Molecule `, \ :class:`Molecules `, \ [:class:`Molecule `], \ - :class:`System ` - A Molecule, Molecules object, a list of Molecule objects, or a System containing molecules. + :class:`System `, \ + :class:`SearchResult ` + A Molecule, Molecules object, a list of Molecule objects, a System, + or a SearchResult containing molecules. """ - # Whether the molecules are in a Sire container. - is_sire_container = False + from ._search_result import SearchResult as _SearchResult + from sire.legacy.Mol import SelectorMol as _SelectorMol + + # Whether this is a selector mol object. + is_selector_mol = False # Convert tuple to a list. if isinstance(molecules, tuple): @@ -583,6 +588,17 @@ def addMolecules(self, molecules): ): molecules = _Molecules(molecules) + # A SearchResult object. + elif isinstance(molecules, _SearchResult): + if isinstance(molecules._sire_object, _SelectorMol): + is_selector_mol = True + pass + else: + raise ValueError( + "Invalid 'SearchResult' object. Can only add a molecule " + "search, i.e. a wrapped 'sire.legacy.Mol.SelectorMol'." + ) + # Invalid argument. else: raise TypeError( @@ -619,7 +635,11 @@ def addMolecules(self, molecules): ) # Add the molecules to the system. - self._sire_object.add(molecules._sire_object, _SireMol.MGName("all")) + if is_selector_mol: + for mol in molecules: + self._sire_object.add(mol._sire_object, _SireMol.MGName("all")) + else: + self._sire_object.add(molecules._sire_object, _SireMol.MGName("all")) # Reset the index mappings. self._reset_mappings() From 29aff165b7bd4655fc89b8e47ed30533dc5f3b03 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 28 Mar 2024 11:06:42 +0000 Subject: [PATCH 067/121] Need to exlude URLs from glob. --- python/BioSimSpace/IO/_io.py | 5 ++++- python/BioSimSpace/Sandpit/Exscientia/IO/_io.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/IO/_io.py b/python/BioSimSpace/IO/_io.py index 433781188..2e0567efc 100644 --- a/python/BioSimSpace/IO/_io.py +++ b/python/BioSimSpace/IO/_io.py @@ -450,7 +450,10 @@ def readMolecules( # Glob all files to catch wildcards. new_files = [] for file in files: - new_files += _glob(file) + if not file.startswith(("http", "www")): + new_files += _glob(file) + else: + new_files.append(file) files = new_files # Validate the molecule unwrapping flag. diff --git a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py index 433781188..2e0567efc 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py +++ b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py @@ -450,7 +450,10 @@ def readMolecules( # Glob all files to catch wildcards. new_files = [] for file in files: - new_files += _glob(file) + if not file.startswith(("http", "www")): + new_files += _glob(file) + else: + new_files.append(file) files = new_files # Validate the molecule unwrapping flag. From 9b6cf96e71e847069bb406127bbf1bd218a09bf0 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 28 Mar 2024 12:11:15 +0000 Subject: [PATCH 068/121] Convert all single string parameters to a list. --- python/BioSimSpace/IO/_io.py | 4 ++-- python/BioSimSpace/Sandpit/Exscientia/IO/_io.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/BioSimSpace/IO/_io.py b/python/BioSimSpace/IO/_io.py index 2e0567efc..ff475f349 100644 --- a/python/BioSimSpace/IO/_io.py +++ b/python/BioSimSpace/IO/_io.py @@ -431,9 +431,9 @@ def readMolecules( ) _has_gmx_warned = True + # Convert a single string to a list. if isinstance(files, str): - if not files.startswith(("http", "www")): - files = [files] + files = [files] # Check that all arguments are of type 'str'. if isinstance(files, (list, tuple)): diff --git a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py index 2e0567efc..ff475f349 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py +++ b/python/BioSimSpace/Sandpit/Exscientia/IO/_io.py @@ -431,9 +431,9 @@ def readMolecules( ) _has_gmx_warned = True + # Convert a single string to a list. if isinstance(files, str): - if not files.startswith(("http", "www")): - files = [files] + files = [files] # Check that all arguments are of type 'str'. if isinstance(files, (list, tuple)): From 33bdd2dd3c339a304285a6f8fa5c5eb030a13937 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 28 Mar 2024 13:23:45 +0000 Subject: [PATCH 069/121] Need to use tishake=1 and an appropriate noshakemask in vacuum. --- python/BioSimSpace/Process/_amber.py | 21 +++++++--- python/BioSimSpace/_Config/_amber.py | 58 +++++++++++++++++++++------- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index 901319a8a..a0fa1300c 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -160,6 +160,12 @@ def __init__( if not isinstance(is_gpu, bool): raise TypeError("'is_gpu' must be of type 'bool'") + # Check whether this is a vacuum simulation. + is_vacuum = not ( + _AmberConfig.hasBox(self._system, self._property_map) + or _AmberConfig.hasWater(self._system) + ) + # If the path to the executable wasn't specified, then search # for it in AMBERHOME and the PATH. if exe is None: @@ -168,12 +174,6 @@ def __init__( else: is_free_energy = False - # Check whether this is a vacuum simulation. - is_vacuum = not ( - _AmberConfig.hasBox(self._system, self._property_map) - or _AmberConfig.hasWater(self._system) - ) - self._exe = _find_exe( is_gpu=is_gpu, is_free_energy=is_free_energy, is_vacuum=is_vacuum ) @@ -184,6 +184,15 @@ def __init__( else: raise IOError("AMBER executable doesn't exist: '%s'" % exe) + # pmemd.cuda doesn't support vacuum free-energy simulations. + if isinstance(protocol, _FreeEnergyMixin): + is_cuda = "cuda" in self._exe.lower() + + if is_cuda and is_vacuum: + _warnings.warn( + "pmemd.cuda doesn't support vacuum free-energy simulations!" + ) + if not isinstance(explicit_dummies, bool): raise TypeError("'explicit_dummies' must be of type 'bool'") self._explicit_dummies = explicit_dummies diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index 2dd446749..ed8269633 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -123,6 +123,14 @@ def createConfig( if not all(isinstance(line, str) for line in extra_lines): raise TypeError("Lines in 'extra_lines' must be of type 'str'.") + # Vaccum simulation. + if not self.hasBox(self._system, self._property_map) or not self.hasWater( + self._system + ): + is_vacuum = True + else: + is_vacuum = False + # Initialise the protocol lines. protocol_lines = [] @@ -173,8 +181,15 @@ def createConfig( # Report energies every 100 steps. protocol_dict["ntpr"] = 100 else: - # Define the timestep + # Get the time step. timestep = self._protocol.getTimeStep().picoseconds().value() + # For free-energy calculations, we can only use a 1fs time step in + # vacuum. + if isinstance(self._protocol, _FreeEnergyMixin): + if is_vacuum and timestep > 0.001: + raise ValueError( + "AMBER free-energy calculations in vacuum must use a 1fs time step." + ) # Set the integration time step. protocol_dict["dt"] = f"{timestep:.3f}" # Number of integration steps. @@ -368,7 +383,7 @@ def createConfig( protocol_dict = { **protocol_dict, **self._generate_amber_fep_masks( - timestep, explicit_dummies=explicit_dummies + self._system, is_vacuum, explicit_dummies=explicit_dummies ), } @@ -459,7 +474,7 @@ def _create_restraint_mask(self, atom_idxs): return restraint_mask - def _generate_amber_fep_masks(self, timestep, explicit_dummies=False): + def _generate_amber_fep_masks(self, system, is_vacuum, explicit_dummies=False): """ Internal helper function which generates timasks and scmasks based on the system. @@ -467,9 +482,11 @@ def _generate_amber_fep_masks(self, timestep, explicit_dummies=False): Parameters ---------- - timestep : [float] - The timestep in ps for the FEP perturbation. Generates a different - mask based on this. + system : :class:`System ` + The molecular system. + + is_vacuum : bool + Whether this is a vacuum simulation. explicit_dummies : bool Whether to keep the dummy atoms explicit at the endstates or remove them. @@ -509,19 +526,34 @@ def _generate_amber_fep_masks(self, timestep, explicit_dummies=False): ti0_indices = mcs0_indices + dummy0_indices ti1_indices = mcs1_indices + dummy1_indices - # SHAKE should be used for timestep > 2 fs. - if timestep is not None and timestep >= 0.002: - no_shake_mask = "" - else: - no_shake_mask = _amber_mask_from_indices(ti0_indices + ti1_indices) - # Create an option dict with amber masks generated from the above indices. option_dict = { "timask1": f'"{_amber_mask_from_indices(ti0_indices)}"', "timask2": f'"{_amber_mask_from_indices(ti1_indices)}"', "scmask1": f'"{_amber_mask_from_indices(dummy0_indices)}"', "scmask2": f'"{_amber_mask_from_indices(dummy1_indices)}"', - "noshakemask": f'"{no_shake_mask}"', + "tishake": 1 if is_vacuum else 0, } + # Add a noshakemask for the perturbed residue. + if is_vacuum: + # Get the perturbable molecules. + pert_mols = system.getPerturbableMolecules() + + # Initialise the noshakemask string. + noshakemask = "" + + # Loop over all perturbable molecules and add residues to the mask. + for mol in pert_mols: + if noshakemask == "": + noshakemask += ":" + for res in mol.getResidues(): + noshakemask += f"{system.getIndex(res) + 1}," + + # Strip the trailing comma. + noshakemask = noshakemask[:-1] + + # Add the noshakemask to the option dict. + option_dict["noshakemask"] = f'"{noshakemask}"' + return option_dict From 188fb683c2685b059e2aaa7195921a508844cd7d Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 28 Mar 2024 14:33:11 +0000 Subject: [PATCH 070/121] Use is_vacuum flag. --- python/BioSimSpace/_Config/_amber.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index ed8269633..b5d782fa1 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -203,9 +203,7 @@ def createConfig( protocol_dict["ntf"] = 2 # Periodic boundary conditions. - if not self.hasBox(self._system, self._property_map) or not self.hasWater( - self._system - ): + if is_vacuum: # No periodic box. protocol_dict["ntb"] = 0 # Non-bonded cut-off. From 5d3ed74e8731a9709685bb5ec2f5b1d3daadcdf8 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 8 Apr 2024 09:57:33 +0100 Subject: [PATCH 071/121] Remove boa and packaging pins. [ci skip] --- .github/workflows/devel.yaml | 2 +- .github/workflows/main.yaml | 2 +- .github/workflows/pr.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index 31869037b..9aa98c8e2 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -49,7 +49,7 @@ jobs: run: git clone -b devel https://github.com/openbiosim/biosimspace # - name: Setup Conda - run: mamba install -y -c conda-forge boa=0.16 anaconda-client packaging=21 pip-requirements-parser + run: mamba install -y -c conda-forge boa anaconda-client packaging pip-requirements-parser # - name: Update Conda recipe run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index a5c441797..f98022f4f 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -43,7 +43,7 @@ jobs: run: git clone -b main https://github.com/openbiosim/biosimspace # - name: Setup Conda - run: mamba install -y -c conda-forge boa=0.16 anaconda-client packaging=21 pip-requirements-parser + run: mamba install -y -c conda-forge boa anaconda-client packaging pip-requirements-parser # - name: Update Conda recipe run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 3ef002f5e..ead208b87 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -53,7 +53,7 @@ jobs: run: git clone -b ${{ github.head_ref }} --single-branch https://github.com/${{ env.REPO }} biosimspace # - name: Setup Conda - run: mamba install -y -c conda-forge boa=0.16 anaconda-client packaging=21 pip-requirements-parser + run: mamba install -y -c conda-forge boa anaconda-client packaging pip-requirements-parser # - name: Update Conda recipe run: python ${{ github.workspace }}/biosimspace/actions/update_recipe.py From e2919b57df325dc990f84e0139123f7b97603944 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 8 Apr 2024 10:22:41 +0100 Subject: [PATCH 072/121] Filter base units list by type and base class. [closes #269] --- .../Sandpit/Exscientia/Types/_base_units.py | 10 ++++++++-- python/BioSimSpace/Types/_base_units.py | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Types/_base_units.py b/python/BioSimSpace/Sandpit/Exscientia/Types/_base_units.py index 931668f84..267cb5516 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Types/_base_units.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Types/_base_units.py @@ -24,6 +24,8 @@ __all__ = ["_base_units", "_base_dimensions", "_sire_units_locals"] +import sys as _sys + from ._angle import * from ._area import * from ._charge import * @@ -33,13 +35,17 @@ from ._temperature import * from ._time import * from ._volume import * - -import sys as _sys +from ._type import Type as _Type _namespace = _sys.modules[__name__] # Create the list of base unit types. _base_units = [getattr(_namespace, var) for var in dir() if var[0] != "_"] +# Filter out any non-Type objects. (This can happen when BioSimSpace is +# wrapped by other tools, e.g. Maize.) +_base_units = [ + unit for unit in _base_units if isinstance(unit, type) and unit.__base__ == _Type +] _base_dimensions = {} for unit in _base_units: diff --git a/python/BioSimSpace/Types/_base_units.py b/python/BioSimSpace/Types/_base_units.py index 931668f84..267cb5516 100644 --- a/python/BioSimSpace/Types/_base_units.py +++ b/python/BioSimSpace/Types/_base_units.py @@ -24,6 +24,8 @@ __all__ = ["_base_units", "_base_dimensions", "_sire_units_locals"] +import sys as _sys + from ._angle import * from ._area import * from ._charge import * @@ -33,13 +35,17 @@ from ._temperature import * from ._time import * from ._volume import * - -import sys as _sys +from ._type import Type as _Type _namespace = _sys.modules[__name__] # Create the list of base unit types. _base_units = [getattr(_namespace, var) for var in dir() if var[0] != "_"] +# Filter out any non-Type objects. (This can happen when BioSimSpace is +# wrapped by other tools, e.g. Maize.) +_base_units = [ + unit for unit in _base_units if isinstance(unit, type) and unit.__base__ == _Type +] _base_dimensions = {} for unit in _base_units: From 7f11fbdd2ffdd146716378676f844701acb99c17 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 8 Apr 2024 14:16:01 +0100 Subject: [PATCH 073/121] Make sure kwargs are supported in __init__. --- python/BioSimSpace/Process/_gromacs.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index 18ac8230f..ec6fb6dea 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -88,6 +88,7 @@ def __init__( ignore_warnings=False, show_errors=True, checkpoint_file=None, + **kwargs, ): """ Constructor. @@ -147,6 +148,9 @@ def __init__( The path to a checkpoint file from a previous run. This can be used to continue an existing simulation. Currently we only support the use of checkpoint files for Equilibration protocols. + + kwargs : dict + Additional keyword arguments. """ # Call the base class constructor. @@ -243,7 +247,7 @@ def __init__( # Now set up the working directory for the process. self._setup(**kwargs) - def _setup(self): + def _setup(self, **kwargs): """Setup the input files and working directory ready for simulation.""" # Create the input files... From 044ad4a565d72e0305ca09f7e79a978a7400f1d0 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 8 Apr 2024 15:18:44 +0100 Subject: [PATCH 074/121] Handle vaccum FEP simulations with pmemd and pmemd.cuda. --- python/BioSimSpace/Process/_amber.py | 104 +++++++++++++++------------ python/BioSimSpace/_Config/_amber.py | 60 +++++++++------- 2 files changed, 92 insertions(+), 72 deletions(-) diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index a0fa1300c..9ca469071 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -161,11 +161,14 @@ def __init__( raise TypeError("'is_gpu' must be of type 'bool'") # Check whether this is a vacuum simulation. - is_vacuum = not ( + self._is_vacuum = not ( _AmberConfig.hasBox(self._system, self._property_map) or _AmberConfig.hasWater(self._system) ) + # Flag to indicate whether the original system has a box. + self._has_box = _AmberConfig.hasBox(self._system, self._property_map) + # If the path to the executable wasn't specified, then search # for it in AMBERHOME and the PATH. if exe is None: @@ -175,7 +178,7 @@ def __init__( is_free_energy = False self._exe = _find_exe( - is_gpu=is_gpu, is_free_energy=is_free_energy, is_vacuum=is_vacuum + is_gpu=is_gpu, is_free_energy=is_free_energy, is_vacuum=self._is_vacuum ) else: # Make sure executable exists. @@ -184,14 +187,16 @@ def __init__( else: raise IOError("AMBER executable doesn't exist: '%s'" % exe) - # pmemd.cuda doesn't support vacuum free-energy simulations. - if isinstance(protocol, _FreeEnergyMixin): - is_cuda = "cuda" in self._exe.lower() - - if is_cuda and is_vacuum: - _warnings.warn( - "pmemd.cuda doesn't support vacuum free-energy simulations!" - ) + # Is this a CUDA enabled version of AMBER? + if "cuda" in self._exe.lower(): + self._is_pmemd_cuda = True + self._is_pmemd = False + else: + self._is_pmemd_cuda = False + if "pmemd" in self._exe.lower(): + self._is_pmemd = True + else: + self._is_pmemd = False if not isinstance(explicit_dummies, bool): raise TypeError("'explicit_dummies' must be of type 'bool'") @@ -270,6 +275,33 @@ def _setup(self, **kwargs): "perturbable molecule!" ) + # If this is vacuum simulation with pmemd.cuda then + # we need to add a simulation box. + if self._is_vacuum and self._is_pmemd_cuda: + # Get the existing box information. + box, _ = system.getBox() + + # We need to add a box. + if box is None: + from ..Box import cubic as _cubic + from ..Units.Length import angstrom as _angstrom + + # Get the bounding box of the system. + box_min, box_max = system.getAxisAlignedBoundingBox() + + # Work out the box size from the difference in the coordinates. + box_size = [y - x for x, y in zip(box_min, box_max)] + + # Work out the size of the box assuming an 8 Angstrom non-bonded cutoff. + padding = 8 * _angstrom + box_length = max(box_size) + padding + # Small box fix. Should be patched in future versions of pmemd.cuda. + if box_length < 30 * _angstrom: + box_length = 30 * _angstrom + + # Set the simulation box. + system.setBox(*_cubic(box_length)) + # Apply SOMD1 compatibility to the perturbation. if ( "somd1_compatibility" in kwargs @@ -283,6 +315,7 @@ def _setup(self, **kwargs): system, explicit_dummies=self._explicit_dummies ) self._squashed_system = system + else: # Check for perturbable molecules and convert to the chosen end state. system = self._checkPerturbable(system) @@ -341,12 +374,6 @@ def _setup(self, **kwargs): def _generate_config(self): """Generate AMBER configuration file strings.""" - # Is this a CUDA enabled version of AMBER? - if "cuda" in self._exe.lower(): - is_pmemd_cuda = True - else: - is_pmemd_cuda = False - extra_options = self._extra_options.copy() extra_lines = self._extra_lines.copy() @@ -388,7 +415,8 @@ def _generate_config(self): # Create the configuration. self.setConfig( amber_config.createConfig( - is_pmemd_cuda=is_pmemd_cuda, + is_pmemd=self._is_pmemd, + is_pmemd_cuda=self._is_pmemd_cuda, explicit_dummies=self._explicit_dummies, extra_options=extra_options, extra_lines=extra_lines, @@ -577,12 +605,13 @@ def getSystem(self, block="AUTO"): self._mapping = mapping # Update the box information in the original system. - if "space" in new_system._sire_object.propertyKeys(): - box = new_system._sire_object.property("space") - if box.isPeriodic(): - old_system._sire_object.setProperty( - self._property_map.get("space", "space"), box - ) + if self._has_box: + if "space" in new_system._sire_object.propertyKeys(): + box = new_system._sire_object.property("space") + if box.isPeriodic(): + old_system._sire_object.setProperty( + self._property_map.get("space", "space"), box + ) return old_system @@ -705,11 +734,12 @@ def getFrame(self, index): self._mapping = mapping # Update the box information in the original system. - if "space" in new_system._sire_object.propertyKeys(): - box = new_system._sire_object.property("space") - old_system._sire_object.setProperty( - self._property_map.get("space", "space"), box - ) + if self._has_box: + if "space" in new_system._sire_object.propertyKeys(): + box = new_system._sire_object.property("space") + old_system._sire_object.setProperty( + self._property_map.get("space", "space"), box + ) return old_system @@ -2783,26 +2813,10 @@ def _find_exe(is_gpu=False, is_free_energy=False, is_vacuum=False): if not isinstance(is_vacuum, bool): raise TypeError("'is_vacuum' must be of type 'bool'.") - # It is not possible to use implicit solvent for free energy simulations - # on GPU, so we fall back to pmemd for vacuum free energy simulations. - - if is_gpu and is_free_energy and is_vacuum: - _warnings.warn( - "Implicit solvent is not supported for free energy simulations on GPU. " - "Falling back to pmemd for vacuum free energy simulations." - ) - is_gpu = False - if is_gpu: targets = ["pmemd.cuda"] else: - if is_free_energy and not is_vacuum: - if is_vacuum: - targets = ["pmemd"] - else: - targets = ["pmemd", "pmemd.cuda"] - else: - targets = ["pmemd", "sander"] + targets = ["pmemd", "sander"] # Search for the executable. diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index b5d782fa1..381401fdd 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -79,6 +79,9 @@ def createConfig( version : float The AMBER version. + is_pmemd : bool + Whether the configuration is for a simulation using PMEMD. + is_pmemd_cuda : bool Whether the configuration is for a simulation using PMEMD with CUDA. @@ -104,6 +107,9 @@ def createConfig( if version and not isinstance(version, float): raise TypeError("'version' must be of type 'float'.") + if not isinstance(is_pmemd, bool): + raise TypeError("'is_pmemd' must be of type 'bool'.") + if not isinstance(is_pmemd_cuda, bool): raise TypeError("'is_pmemd_cuda' must be of type 'bool'.") @@ -186,9 +192,9 @@ def createConfig( # For free-energy calculations, we can only use a 1fs time step in # vacuum. if isinstance(self._protocol, _FreeEnergyMixin): - if is_vacuum and timestep > 0.001: + if is_vacuum and not is_pmemd_cuda and timestep > 0.001: raise ValueError( - "AMBER free-energy calculations in vacuum must use a 1fs time step." + "AMBER free-energy calculations in vacuum using pmemd must use a 1fs time step." ) # Set the integration time step. protocol_dict["dt"] = f"{timestep:.3f}" @@ -203,7 +209,9 @@ def createConfig( protocol_dict["ntf"] = 2 # Periodic boundary conditions. - if is_vacuum: + if is_vacuum and not ( + is_pmemd_cuda and isinstance(self._protocol, _FreeEnergyMixin) + ): # No periodic box. protocol_dict["ntb"] = 0 # Non-bonded cut-off. @@ -381,7 +389,11 @@ def createConfig( protocol_dict = { **protocol_dict, **self._generate_amber_fep_masks( - self._system, is_vacuum, explicit_dummies=explicit_dummies + self._system, + is_vacuum, + is_pmemd_cuda, + timestep, + explicit_dummies=explicit_dummies, ), } @@ -472,7 +484,9 @@ def _create_restraint_mask(self, atom_idxs): return restraint_mask - def _generate_amber_fep_masks(self, system, is_vacuum, explicit_dummies=False): + def _generate_amber_fep_masks( + self, system, is_vacuum, is_pmemd_cuda, timestep, explicit_dummies=False + ): """ Internal helper function which generates timasks and scmasks based on the system. @@ -486,6 +500,12 @@ def _generate_amber_fep_masks(self, system, is_vacuum, explicit_dummies=False): is_vacuum : bool Whether this is a vacuum simulation. + is_pmemd_cuda : bool + Whether this is a CUDA simulation. + + timestep : float + The timestep of the simulation in femtoseconds. + explicit_dummies : bool Whether to keep the dummy atoms explicit at the endstates or remove them. @@ -524,34 +544,20 @@ def _generate_amber_fep_masks(self, system, is_vacuum, explicit_dummies=False): ti0_indices = mcs0_indices + dummy0_indices ti1_indices = mcs1_indices + dummy1_indices + # SHAKE should be used for timestep >= 2 fs. + if timestep is None or timestep >= 0.002: + no_shake_mask = "" + else: + no_shake_mask = _amber_mask_from_indices(ti0_indices + ti1_indices) + # Create an option dict with amber masks generated from the above indices. option_dict = { "timask1": f'"{_amber_mask_from_indices(ti0_indices)}"', "timask2": f'"{_amber_mask_from_indices(ti1_indices)}"', "scmask1": f'"{_amber_mask_from_indices(dummy0_indices)}"', "scmask2": f'"{_amber_mask_from_indices(dummy1_indices)}"', - "tishake": 1 if is_vacuum else 0, + "tishake": 0 if is_pmemd_cuda else 1, + "noshakemask": f'"{no_shake_mask}"', } - # Add a noshakemask for the perturbed residue. - if is_vacuum: - # Get the perturbable molecules. - pert_mols = system.getPerturbableMolecules() - - # Initialise the noshakemask string. - noshakemask = "" - - # Loop over all perturbable molecules and add residues to the mask. - for mol in pert_mols: - if noshakemask == "": - noshakemask += ":" - for res in mol.getResidues(): - noshakemask += f"{system.getIndex(res) + 1}," - - # Strip the trailing comma. - noshakemask = noshakemask[:-1] - - # Add the noshakemask to the option dict. - option_dict["noshakemask"] = f'"{noshakemask}"' - return option_dict From f1fb052cbe66ba3db63182187cdbc3ea9e502b66 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 8 Apr 2024 16:04:43 +0100 Subject: [PATCH 075/121] Remove the parameters property before creating a partial molecule. [ref OpenBioSim/sire#183] --- python/BioSimSpace/Align/_merge.py | 6 ++++++ python/BioSimSpace/Sandpit/Exscientia/Align/_merge.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/python/BioSimSpace/Align/_merge.py b/python/BioSimSpace/Align/_merge.py index f43eb3a5e..f4693e83e 100644 --- a/python/BioSimSpace/Align/_merge.py +++ b/python/BioSimSpace/Align/_merge.py @@ -1417,6 +1417,12 @@ def _removeDummies(molecule, is_lambda1): is_lambda1=is_lambda1, generate_intrascale=True ) + # Remove the parameters property, if it exists. + if "parameters" in molecule._sire_object.propertyKeys(): + molecule._sire_object = ( + molecule._sire_object.edit().removeProperty("parameters").commit() + ) + # Set the coordinates to those at lambda = 0 molecule._sire_object = ( molecule._sire_object.edit().setProperty("coordinates", coordinates).commit() diff --git a/python/BioSimSpace/Sandpit/Exscientia/Align/_merge.py b/python/BioSimSpace/Sandpit/Exscientia/Align/_merge.py index 859662a16..29dbf731f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Align/_merge.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Align/_merge.py @@ -1561,6 +1561,12 @@ def _removeDummies(molecule, is_lambda1): is_lambda1=is_lambda1, generate_intrascale=True ) + # Remove the parameters property, if it exists. + if "parameters" in molecule._sire_object.propertyKeys(): + molecule._sire_object = ( + molecule._sire_object.edit().removeProperty("parameters").commit() + ) + # Set the coordinates to those at lambda = 0 molecule._sire_object = ( molecule._sire_object.edit().setProperty("coordinates", coordinates).commit() From 185ccaa54b5c575b327a0361c5a1fcc3260e1d3b Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 8 Apr 2024 16:10:28 +0100 Subject: [PATCH 076/121] Remove the parameters property when extracting a partial molecule. [ref OpenBioSim/sire#183] --- .../Sandpit/Exscientia/_SireWrappers/_molecule.py | 13 ++++++++++--- python/BioSimSpace/_SireWrappers/_molecule.py | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py index 6de6095dd..e9df3208e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py @@ -353,10 +353,17 @@ def extract(self, indices, renumber=False, property_map={}): for idx in indices_: selection.select(idx) + # Store the Sire molecule. + sire_mol = self._sire_object + + # Remove the "parameters" property, if it exists. + if sire_mol.hasProperty("parameters"): + sire_mol = ( + sire_mol.edit().removeProperty("parameters").commit().molecule() + ) + partial_mol = ( - _SireMol.PartialMolecule(self._sire_object, selection) - .extract() - .molecule() + _SireMol.PartialMolecule(sire_mol, selection).extract().molecule() ) except Exception as e: msg = "Unable to create partial molecule!" diff --git a/python/BioSimSpace/_SireWrappers/_molecule.py b/python/BioSimSpace/_SireWrappers/_molecule.py index a7f510e17..efa7fb4fa 100644 --- a/python/BioSimSpace/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/_SireWrappers/_molecule.py @@ -353,10 +353,17 @@ def extract(self, indices, renumber=False, property_map={}): for idx in indices_: selection.select(idx) + # Store the Sire molecule. + sire_mol = self._sire_object + + # Remove the "parameters" property, if it exists. + if sire_mol.hasProperty("parameters"): + sire_mol = ( + sire_mol.edit().removeProperty("parameters").commit().molecule() + ) + partial_mol = ( - _SireMol.PartialMolecule(self._sire_object, selection) - .extract() - .molecule() + _SireMol.PartialMolecule(sire_mol, selection).extract().molecule() ) except Exception as e: msg = "Unable to create partial molecule!" From 698473b6382679f98b16f0dcd0d9296536843dfb Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 9 Apr 2024 10:45:40 +0100 Subject: [PATCH 077/121] Add pmemd single-point energy tests. --- tests/Process/test_amber.py | 141 ++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/tests/Process/test_amber.py b/tests/Process/test_amber.py index bd7a8a703..6c726264d 100644 --- a/tests/Process/test_amber.py +++ b/tests/Process/test_amber.py @@ -1,7 +1,9 @@ from collections import OrderedDict +import math import pytest import shutil +import socket import BioSimSpace as BSS @@ -44,6 +46,17 @@ def perturbable_system(): ) +@pytest.fixture(scope="module") +def solvated_perturbable_system(): + """Re-use the same solvated perturbable system for each test.""" + return BSS.IO.readPerturbableSystem( + f"{url}/solvated_perturbable_system0.prm7", + f"{url}/solvated_perturbable_system0.rst7", + f"{url}/solvated_perturbable_system1.prm7", + f"{url}/solvated_perturbable_system1.rst7", + ) + + @pytest.mark.skipif(has_amber is False, reason="Requires AMBER to be installed.") @pytest.mark.parametrize("restraint", restraints) def test_minimise(system, restraint): @@ -364,3 +377,131 @@ def test_parse_fep_output(perturbable_system, protocol): else: assert len(records_sc0) == 0 assert len(records_sc1) != 0 + + +@pytest.mark.skipif( + socket.gethostname() != "porridge", + reason="Local test requiring pmemd installation.", +) +def test_pmemd(system): + """Single-point energy tests for pmemd.""" + + # Path to the pmemd conda environment bin directory. + bin_dir = "/home/lester/.conda/envs/pmemd/bin" + + # Single-point minimisation protocol. + protocol = BSS.Protocol.Minimisation(steps=1) + + # First perform single-point comparisons in solvent. + + # Compute the single-point energy using sander. + process = BSS.Process.Amber(system, protocol) + process.start() + process.wait() + assert not process.isError() + nrg_sander = process.getTotalEnergy().value() + + # Compute the single-point energy using pmemd. + process = BSS.Process.Amber(system, protocol, exe=f"{bin_dir}/pmemd") + process.start() + process.wait() + assert not process.isError() + nrg_pmemd = process.getTotalEnergy().value() + + # Compute the single-point energy using pmemd.cuda. + process = BSS.Process.Amber(system, protocol, exe=f"{bin_dir}/pmemd.cuda") + process.start() + process.wait() + assert not process.isError() + nrg_pmemd_cuda = process.getTotalEnergy().value() + + # Check that the energies are the same. + assert math.isclose(nrg_sander, nrg_pmemd, rel_tol=1e-4) + assert math.isclose(nrg_sander, nrg_pmemd_cuda, rel_tol=1e-4) + + # Now perform single-point comparisons in vacuum. + + vac_system = system[0].toSystem() + + # Compute the single-point energy using sander. + process = BSS.Process.Amber(vac_system, protocol) + process.start() + process.wait() + assert not process.isError() + nrg_sander = process.getTotalEnergy().value() + + # Compute the single-point energy using pmemd. + process = BSS.Process.Amber(vac_system, protocol, exe=f"{bin_dir}/pmemd") + process.start() + process.wait() + assert not process.isError() + nrg_pmemd = process.getTotalEnergy().value() + + # Compute the single-point energy using pmemd.cuda. + process = BSS.Process.Amber(vac_system, protocol, exe=f"{bin_dir}/pmemd.cuda") + process.start() + process.wait() + assert not process.isError() + nrg_pmemd_cuda = process.getTotalEnergy().value() + + # Check that the energies are the same. + assert math.isclose(nrg_sander, nrg_pmemd, rel_tol=1e-4) + assert math.isclose(nrg_sander, nrg_pmemd_cuda, rel_tol=1e-4) + + +@pytest.mark.skipif( + socket.gethostname() != "porridge", + reason="Local test requiring pmemd installation.", +) +def test_pmemd_fep(solvated_perturbable_system): + """Single-point FEP energy tests for pmemd.""" + + # Path to the pmemd conda environment bin directory. + bin_dir = "/home/lester/.conda/envs/pmemd/bin" + + # Single-point minimisation protocol. + protocol = BSS.Protocol.FreeEnergyMinimisation(steps=1) + + # First perform single-point comparisons in solvent. + + # Compute the single-point energy using pmemd. + process = BSS.Process.Amber( + solvated_perturbable_system, protocol, exe=f"{bin_dir}/pmemd" + ) + process.start() + process.wait() + assert not process.isError() + nrg_pmemd = process.getTotalEnergy().value() + + # Compute the single-point energy using pmemd.cuda. + process = BSS.Process.Amber( + solvated_perturbable_system, protocol, exe=f"{bin_dir}/pmemd.cuda" + ) + process.start() + process.wait() + assert not process.isError() + nrg_pmemd_cuda = process.getTotalEnergy().value() + + # Check that the energies are the same. + assert math.isclose(nrg_pmemd, nrg_pmemd_cuda, rel_tol=1e-4) + + # Now perform single-point comparisons in vacuum. + + vac_system = solvated_perturbable_system[0].toSystem() + + # Compute the single-point energy using pmemd. + process = BSS.Process.Amber(vac_system, protocol, exe=f"{bin_dir}/pmemd") + process.start() + process.wait() + assert not process.isError() + nrg_pmemd = process.getTotalEnergy().value() + + # Compute the single-point energy using pmemd.cuda. + process = BSS.Process.Amber(vac_system, protocol, exe=f"{bin_dir}/pmemd.cuda") + process.start() + process.wait() + assert not process.isError() + nrg_pmemd_cuda = process.getTotalEnergy().value() + + # Vaccum energies currently differ between pmemd and pmemd.cuda. + assert not math.isclose(nrg_pmemd, nrg_pmemd_cuda, rel_tol=1e-4) From 8d781d4eb1cbb634cd88a827d46b37c550eac073 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 9 Apr 2024 10:48:52 +0100 Subject: [PATCH 078/121] Update Python variants to match Sire. --- .github/workflows/devel.yaml | 2 +- .github/workflows/main.yaml | 2 +- .github/workflows/pr.yaml | 2 +- recipes/biosimspace/template.yaml | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index 9aa98c8e2..b6c7f1bdf 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -13,7 +13,7 @@ jobs: max-parallel: 9 fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11", "3.12"] platform: - { name: "windows", os: "windows-latest", shell: "pwsh" } - { name: "linux", os: "ubuntu-latest", shell: "bash -l {0}" } diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f98022f4f..ad560f419 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -16,7 +16,7 @@ jobs: max-parallel: 9 fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11", "3.12"] platform: - { name: "windows", os: "windows-latest", shell: "pwsh" } - { name: "linux", os: "ubuntu-latest", shell: "bash -l {0}" } diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index ead208b87..6d4c7b1a6 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -12,7 +12,7 @@ jobs: max-parallel: 9 fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11", "3.12"] platform: - { name: "windows", os: "windows-latest", shell: "pwsh" } - { name: "linux", os: "ubuntu-latest", shell: "bash -l {0}" } diff --git a/recipes/biosimspace/template.yaml b/recipes/biosimspace/template.yaml index efcb2c141..4e3a7edeb 100644 --- a/recipes/biosimspace/template.yaml +++ b/recipes/biosimspace/template.yaml @@ -27,18 +27,18 @@ test: - SIRE_SILENT_PHONEHOME requires: - pytest <8 - - black 23 # [linux and x86_64 and py==311] - - pytest-black # [linux and x86_64 and py==311] + - black 23 # [linux and x86_64 and py==312] + - pytest-black # [linux and x86_64 and py==312] - ambertools # [linux and x86_64] - gromacs # [linux and x86_64] - requests imports: - BioSimSpace source_files: - - python/BioSimSpace # [linux and x86_64 and py==311] + - python/BioSimSpace # [linux and x86_64 and py==312] - tests commands: - - pytest -vvv --color=yes --black python/BioSimSpace # [linux and x86_64 and py==311] + - pytest -vvv --color=yes --black python/BioSimSpace # [linux and x86_64 and py==312] - pytest -vvv --color=yes --import-mode=importlib tests about: From 77d45240329471c77261e44ccc215c95af52f49b Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 9 Apr 2024 10:55:38 +0100 Subject: [PATCH 079/121] Fix Python variant platform excludes. --- .github/workflows/devel.yaml | 9 +++++---- .github/workflows/main.yaml | 4 ++++ .github/workflows/pr.yaml | 8 ++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/devel.yaml b/.github/workflows/devel.yaml index b6c7f1bdf..216bbdc29 100644 --- a/.github/workflows/devel.yaml +++ b/.github/workflows/devel.yaml @@ -19,14 +19,15 @@ jobs: - { name: "linux", os: "ubuntu-latest", shell: "bash -l {0}" } - { name: "macos", os: "macos-latest", shell: "bash -l {0}" } exclude: - # Exclude all but the latest Python from all but Linux + # Exclude all but the latest Python from all + # but Linux - platform: { name: "macos", os: "macos-latest", shell: "bash -l {0}" } - python-version: "3.9" - - platform: { name: "windows", os: "windows-latest", shell: "pwsh" } - python-version: "3.9" + python-version: "3.12" # MacOS can't run 3.12 yet... We want 3.10 and 3.11 - platform: { name: "windows", os: "windows-latest", shell: "pwsh" } python-version: "3.10" + - platform: { name: "windows", os: "windows-latest", shell: "pwsh" } + python-version: "3.11" environment: name: biosimspace-build defaults: diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index ad560f419..d453c2dd8 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -21,6 +21,10 @@ jobs: - { name: "windows", os: "windows-latest", shell: "pwsh" } - { name: "linux", os: "ubuntu-latest", shell: "bash -l {0}" } - { name: "macos", os: "macos-latest", shell: "bash -l {0}" } + exclude: + - platform: + { name: "macos", os: "macos-latest", shell: "bash -l {0}" } + python-version: "3.12" # MacOS can't run 3.12 yet... environment: name: biosimspace-build defaults: diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 6d4c7b1a6..476d7a0fa 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -22,14 +22,14 @@ jobs: # but Linux - platform: { name: "macos", os: "macos-latest", shell: "bash -l {0}" } - python-version: "3.9" + python-version: "3.10" - platform: { name: "windows", os: "windows-latest", shell: "pwsh" } - python-version: "3.9" + python-version: "3.10" - platform: { name: "macos", os: "macos-latest", shell: "bash -l {0}" } - python-version: "3.10" + python-version: "3.12" # MacOS can't run 3.12 yet... - platform: { name: "windows", os: "windows-latest", shell: "pwsh" } - python-version: "3.10" + python-version: "3.11" environment: name: biosimspace-build defaults: From f1d4223108840ae445aa7803f2b8709b8860f8ca Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 9 Apr 2024 11:12:47 +0100 Subject: [PATCH 080/121] Upgrade versioneer. --- python/BioSimSpace/__init__.py | 3 + python/BioSimSpace/_version.py | 449 +++++++---- python/versioneer.py | 1350 +++++++++++++++++++++----------- 3 files changed, 1164 insertions(+), 638 deletions(-) diff --git a/python/BioSimSpace/__init__.py b/python/BioSimSpace/__init__.py index b14f0228f..62bbb7974 100644 --- a/python/BioSimSpace/__init__.py +++ b/python/BioSimSpace/__init__.py @@ -257,3 +257,6 @@ def _isVerbose(): __version__ = get_versions()["version"] del get_versions + +from . import _version +__version__ = _version.get_versions()['version'] diff --git a/python/BioSimSpace/_version.py b/python/BioSimSpace/_version.py index 043ff7784..e6befd83e 100644 --- a/python/BioSimSpace/_version.py +++ b/python/BioSimSpace/_version.py @@ -1,11 +1,13 @@ + # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) +# This file is released into the public domain. +# Generated by versioneer-0.29 +# https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" @@ -14,9 +16,11 @@ import re import subprocess import sys +from typing import Any, Callable, Dict, List, Optional, Tuple +import functools -def get_keywords(): +def get_keywords() -> Dict[str, str]: """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must @@ -32,8 +36,15 @@ def get_keywords(): class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + parentdir_prefix: str + versionfile_source: str + verbose: bool + -def get_config(): +def get_config() -> VersioneerConfig: """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py @@ -51,41 +62,50 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} -HANDLERS = {} - +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f - return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen( - [c] + args, - cwd=cwd, - env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr else None), - ) + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) break - except EnvironmentError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -96,18 +116,20 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= if verbose: print("unable to find command, tried %s" % (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -116,64 +138,64 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): - return { - "version": dirname[len(parentdir_prefix) :], - "full-revisionid": None, - "dirty": False, - "error": None, - "date": None, - } - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: - print( - "Tried directories %s but none started with prefix %s" - % (str(rootdirs), parentdir_prefix) - ) + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -186,11 +208,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -199,7 +221,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r"\d", r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -207,30 +229,33 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix) :] + r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue if verbose: print("picking %s" % r) - return { - "version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, - "error": None, - "date": date, - } + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") - return { - "version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, - "error": "no suitable tags", - "date": None, - } + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -241,7 +266,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -249,33 +282,57 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command( - GITS, - [ - "describe", - "--tags", - "--dirty", - "--always", - "--long", - "--match", - "%s*" % tag_prefix, - ], - cwd=root, - ) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", f"{tag_prefix}[[:digit:]]*" + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -284,16 +341,17 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[: git_describe.rindex("-dirty")] + git_describe = git_describe[:git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX - mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out + # unparsable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) return pieces # tag @@ -302,12 +360,10 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( - full_tag, - tag_prefix, - ) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix) :] + pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -318,26 +374,27 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ - 0 - ].strip() + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -355,29 +412,78 @@ def render_pep440(pieces): rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] + rendered = "0.post0.dev%d" % pieces["distance"] return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -404,12 +510,41 @@ def render_pep440_post(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -426,7 +561,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -446,7 +581,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -466,26 +601,28 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: - return { - "version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None, - } + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": @@ -495,16 +632,12 @@ def render(pieces, style): else: raise ValueError("unknown style '%s'" % style) - return { - "version": rendered, - "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], - "error": None, - "date": pieces.get("date"), - } + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} -def get_versions(): +def get_versions() -> Dict[str, Any]: """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some @@ -515,7 +648,8 @@ def get_versions(): verbose = cfg.verbose try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) except NotThisMethod: pass @@ -524,16 +658,13 @@ def get_versions(): # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in cfg.versionfile_source.split("/"): + for _ in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: - return { - "version": "0+unknown", - "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None, - } + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None} try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) @@ -547,10 +678,6 @@ def get_versions(): except NotThisMethod: pass - return { - "version": "0+unknown", - "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", - "date": None, - } + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", "date": None} diff --git a/python/versioneer.py b/python/versioneer.py index caa3aaf58..1e3753e63 100644 --- a/python/versioneer.py +++ b/python/versioneer.py @@ -1,4 +1,5 @@ -# Version: 0.18 + +# Version: 0.29 """The Versioneer - like a rocketeer, but for versions. @@ -6,28 +7,54 @@ ============== * like a rocketeer, but for versions! -* https://github.com/warner/python-versioneer +* https://github.com/python-versioneer/python-versioneer * Brian Warner -* License: Public Domain -* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy -* [![Latest Version] -(https://pypip.in/version/versioneer/badge.svg?style=flat) -](https://pypi.python.org/pypi/versioneer/) -* [![Build Status] -(https://travis-ci.org/warner/python-versioneer.png?branch=master) -](https://travis-ci.org/warner/python-versioneer) - -This is a tool for managing a recorded version number in distutils-based +* License: Public Domain (Unlicense) +* Compatible with: Python 3.7, 3.8, 3.9, 3.10, 3.11 and pypy3 +* [![Latest Version][pypi-image]][pypi-url] +* [![Build Status][travis-image]][travis-url] + +This is a tool for managing a recorded version number in setuptools-based python projects. The goal is to remove the tedious and error-prone "update the embedded version string" step from your release process. Making a new release should be as easy as recording a new tag in your version-control system, and maybe making new tarballs. + ## Quick Install -* `pip install versioneer` to somewhere to your $PATH -* add a `[versioneer]` section to your setup.cfg (see below) -* run `versioneer install` in your source tree, commit the results +Versioneer provides two installation modes. The "classic" vendored mode installs +a copy of versioneer into your repository. The experimental build-time dependency mode +is intended to allow you to skip this step and simplify the process of upgrading. + +### Vendored mode + +* `pip install versioneer` to somewhere in your $PATH + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) + * Note that you will need to add `tomli; python_version < "3.11"` to your + build-time dependencies if you use `pyproject.toml` +* run `versioneer install --vendor` in your source tree, commit the results +* verify version information with `python setup.py version` + +### Build-time dependency mode + +* `pip install versioneer` to somewhere in your $PATH + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) +* add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) + to the `requires` key of the `build-system` table in `pyproject.toml`: + ```toml + [build-system] + requires = ["setuptools", "versioneer[toml]"] + build-backend = "setuptools.build_meta" + ``` +* run `versioneer install --no-vendor` in your source tree, commit the results +* verify version information with `python setup.py version` ## Version Identifiers @@ -59,7 +86,7 @@ for example `git describe --tags --dirty --always` reports things like "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has -uncommitted changes. +uncommitted changes). The version identifier is used for multiple purposes: @@ -164,7 +191,7 @@ Some situations are known to cause problems for Versioneer. This details the most significant ones. More can be found on Github -[issues page](https://github.com/warner/python-versioneer/issues). +[issues page](https://github.com/python-versioneer/python-versioneer/issues). ### Subprojects @@ -192,9 +219,9 @@ Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in some later version. -[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking +[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking this issue. The discussion in -[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the +[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the issue from the Versioneer side in more detail. [pip PR#3176](https://github.com/pypa/pip/pull/3176) and [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve @@ -222,30 +249,20 @@ cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into a different virtualenv), so this can be surprising. -[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes +[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes this one, but upgrading to a newer version of setuptools should probably resolve it. -### Unicode version strings - -While Versioneer works (and is continually tested) with both Python 2 and -Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. -Newer releases probably generate unicode version strings on py2. It's not -clear that this is wrong, but it may be surprising for applications when then -write these strings to a network connection or include them in bytes-oriented -APIs like cryptographic checksums. - -[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates -this question. ## Updating Versioneer To upgrade your project to a new release of Versioneer, do the following: * install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg`, if necessary, to include any new configuration settings - indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. -* re-run `versioneer install` in your source tree, to replace +* edit `setup.cfg` and `pyproject.toml`, if necessary, + to include any new configuration settings indicated by the release notes. + See [UPGRADING](./UPGRADING.md) for details. +* re-run `versioneer install --[no-]vendor` in your source tree, to replace `SRC/_version.py` * commit any changed files @@ -262,34 +279,70 @@ direction and include code from all supported VCS systems, reducing the number of intermediate scripts. +## Similar projects + +* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time + dependency +* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of + versioneer +* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools + plugin + ## License To make Versioneer easier to embed, all its code is dedicated to the public domain. The `_version.py` that it creates is also in the public domain. -Specifically, both are released under the Creative Commons "Public Domain -Dedication" license (CC0-1.0), as described in -https://creativecommons.org/publicdomain/zero/1.0/ . -""" +Specifically, both are released under the "Unlicense", as described in +https://unlicense.org/. + +[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg +[pypi-url]: https://pypi.python.org/pypi/versioneer/ +[travis-image]: +https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg +[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer -from __future__ import print_function +""" +# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring +# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements +# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error +# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with +# pylint:disable=attribute-defined-outside-init,too-many-arguments -try: - import configparser -except ImportError: - import ConfigParser as configparser +import configparser import errno import json import os import re import subprocess import sys +from pathlib import Path +from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union +from typing import NoReturn +import functools + +have_tomllib = True +if sys.version_info >= (3, 11): + import tomllib +else: + try: + import tomli as tomllib + except ImportError: + have_tomllib = False class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + versionfile_source: str + versionfile_build: Optional[str] + parentdir_prefix: Optional[str] + verbose: Optional[bool] -def get_root(): + +def get_root() -> str: """Get the project root directory. We require that all commands are run from the project root, i.e. the @@ -297,20 +350,28 @@ def get_root(): """ root = os.path.realpath(os.path.abspath(os.getcwd())) setup_py = os.path.join(root, "setup.py") + pyproject_toml = os.path.join(root, "pyproject.toml") versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + if not ( + os.path.exists(setup_py) + or os.path.exists(pyproject_toml) + or os.path.exists(versioneer_py) + ): # allow 'python path/to/setup.py COMMAND' root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) setup_py = os.path.join(root, "setup.py") + pyproject_toml = os.path.join(root, "pyproject.toml") versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - err = ( - "Versioneer was unable to run the project root directory. " - "Versioneer requires setup.py to be executed from " - "its immediate directory (like 'python setup.py COMMAND'), " - "or in a way that lets it use sys.argv[0] to find the root " - "(like 'python path/to/setup.py COMMAND')." - ) + if not ( + os.path.exists(setup_py) + or os.path.exists(pyproject_toml) + or os.path.exists(versioneer_py) + ): + err = ("Versioneer was unable to run the project root directory. " + "Versioneer requires setup.py to be executed from " + "its immediate directory (like 'python setup.py COMMAND'), " + "or in a way that lets it use sys.argv[0] to find the root " + "(like 'python path/to/setup.py COMMAND').") raise VersioneerBadRootError(err) try: # Certain runtime workflows (setup.py install/develop in a setuptools @@ -319,46 +380,62 @@ def get_root(): # module-import table will cache the first one. So we can't use # os.path.dirname(__file__), as that will find whichever # versioneer.py was first imported, even in later projects. - me = os.path.realpath(os.path.abspath(__file__)) - me_dir = os.path.normcase(os.path.splitext(me)[0]) + my_path = os.path.realpath(os.path.abspath(__file__)) + me_dir = os.path.normcase(os.path.splitext(my_path)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) - if me_dir != vsr_dir: - print( - "Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(me), versioneer_py) - ) + if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): + print("Warning: build in %s is using versioneer.py from %s" + % (os.path.dirname(my_path), versioneer_py)) except NameError: pass return root -def get_config_from_root(root): +def get_config_from_root(root: str) -> VersioneerConfig: """Read the project setup.cfg file to determine Versioneer config.""" - # This might raise EnvironmentError (if setup.cfg is missing), or + # This might raise OSError (if setup.cfg is missing), or # configparser.NoSectionError (if it lacks a [versioneer] section), or # configparser.NoOptionError (if it lacks "VCS="). See the docstring at # the top of versioneer.py for instructions on writing your setup.cfg . - setup_cfg = os.path.join(root, "setup.cfg") - parser = configparser.SafeConfigParser() - with open(setup_cfg, "r") as f: - parser.readfp(f) - VCS = parser.get("versioneer", "VCS") # mandatory - - def get(parser, name): - if parser.has_option("versioneer", name): - return parser.get("versioneer", name) - return None + root_pth = Path(root) + pyproject_toml = root_pth / "pyproject.toml" + setup_cfg = root_pth / "setup.cfg" + section: Union[Dict[str, Any], configparser.SectionProxy, None] = None + if pyproject_toml.exists() and have_tomllib: + try: + with open(pyproject_toml, 'rb') as fobj: + pp = tomllib.load(fobj) + section = pp['tool']['versioneer'] + except (tomllib.TOMLDecodeError, KeyError) as e: + print(f"Failed to load config from {pyproject_toml}: {e}") + print("Try to load it from setup.cfg") + if not section: + parser = configparser.ConfigParser() + with open(setup_cfg) as cfg_file: + parser.read_file(cfg_file) + parser.get("versioneer", "VCS") # raise error if missing + + section = parser["versioneer"] + + # `cast`` really shouldn't be used, but its simplest for the + # common VersioneerConfig users at the moment. We verify against + # `None` values elsewhere where it matters cfg = VersioneerConfig() - cfg.VCS = VCS - cfg.style = get(parser, "style") or "" - cfg.versionfile_source = get(parser, "versionfile_source") - cfg.versionfile_build = get(parser, "versionfile_build") - cfg.tag_prefix = get(parser, "tag_prefix") - if cfg.tag_prefix in ("''", '""'): + cfg.VCS = section['VCS'] + cfg.style = section.get("style", "") + cfg.versionfile_source = cast(str, section.get("versionfile_source")) + cfg.versionfile_build = section.get("versionfile_build") + cfg.tag_prefix = cast(str, section.get("tag_prefix")) + if cfg.tag_prefix in ("''", '""', None): cfg.tag_prefix = "" - cfg.parentdir_prefix = get(parser, "parentdir_prefix") - cfg.verbose = get(parser, "verbose") + cfg.parentdir_prefix = section.get("parentdir_prefix") + if isinstance(section, configparser.SectionProxy): + # Make sure configparser translates to bool + cfg.verbose = section.getboolean("verbose") + else: + cfg.verbose = section.get("verbose") + return cfg @@ -367,41 +444,48 @@ class NotThisMethod(Exception): # these dictionaries contain VCS-specific tools -LONG_VERSION_PY = {} -HANDLERS = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - - def decorate(f): +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f + HANDLERS.setdefault(vcs, {})[method] = f return f - return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen( - [c] + args, - cwd=cwd, - env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr else None), - ) + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) break - except EnvironmentError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -412,28 +496,25 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= if verbose: print("unable to find command, tried %s" % (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode -LONG_VERSION_PY[ - "git" -] = ''' +LONG_VERSION_PY['git'] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) +# This file is released into the public domain. +# Generated by versioneer-0.29 +# https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" @@ -442,9 +523,11 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= import re import subprocess import sys +from typing import Any, Callable, Dict, List, Optional, Tuple +import functools -def get_keywords(): +def get_keywords() -> Dict[str, str]: """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must @@ -460,8 +543,15 @@ def get_keywords(): class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + parentdir_prefix: str + versionfile_source: str + verbose: bool -def get_config(): + +def get_config() -> VersioneerConfig: """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py @@ -479,13 +569,13 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} -HANDLERS = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} @@ -494,22 +584,35 @@ def decorate(f): return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) break - except EnvironmentError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -520,18 +623,20 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, if verbose: print("unable to find command, tried %%s" %% (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %%s (error)" %% dispcmd) print("stdout was %%s" %% stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -540,15 +645,14 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: print("Tried directories %%s but none started with prefix %%s" %% @@ -557,41 +661,48 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -604,11 +715,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %%d @@ -617,7 +728,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%%s', no digits" %% ",".join(refs - tags)) if verbose: @@ -626,6 +737,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue if verbose: print("picking %%s" %% r) return {"version": r, @@ -641,7 +757,12 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -652,8 +773,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %%s not under git control" %% root) @@ -661,24 +789,57 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%%s*" %% tag_prefix], - cwd=root) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", f"{tag_prefix}[[:digit:]]*" + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -720,26 +881,27 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], - cwd=root)[0].strip() + date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -764,23 +926,71 @@ def render_pep440(pieces): return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%%d.g%%s" %% (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%%d" %% pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%%d" %% (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%%d" %% pieces["distance"] + rendered = "0.post0.dev%%d" %% pieces["distance"] return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -807,12 +1017,41 @@ def render_pep440_post(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%%s" %% pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%%s" %% pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -829,7 +1068,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -849,7 +1088,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -869,7 +1108,7 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", @@ -883,10 +1122,14 @@ def render(pieces, style): if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": @@ -901,7 +1144,7 @@ def render(pieces, style): "date": pieces.get("date")} -def get_versions(): +def get_versions() -> Dict[str, Any]: """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some @@ -922,7 +1165,7 @@ def get_versions(): # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): + for _ in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: return {"version": "0+unknown", "full-revisionid": None, @@ -949,41 +1192,48 @@ def get_versions(): @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -996,11 +1246,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -1009,7 +1259,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r"\d", r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -1017,30 +1267,33 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix) :] + r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue if verbose: print("picking %s" % r) - return { - "version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, - "error": None, - "date": date, - } + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") - return { - "version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, - "error": "no suitable tags", - "date": None, - } + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -1051,7 +1304,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -1059,33 +1320,57 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command( - GITS, - [ - "describe", - "--tags", - "--dirty", - "--always", - "--long", - "--match", - "%s*" % tag_prefix, - ], - cwd=root, - ) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", f"{tag_prefix}[[:digit:]]*" + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -1094,16 +1379,17 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[: git_describe.rindex("-dirty")] + git_describe = git_describe[:git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX - mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) return pieces # tag @@ -1112,12 +1398,10 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( - full_tag, - tag_prefix, - ) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix) :] + pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -1128,19 +1412,20 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ - 0 - ].strip() + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces -def do_vcs_install(manifest_in, versionfile_source, ipy): +def do_vcs_install(versionfile_source: str, ipy: Optional[str]) -> None: """Git-specific installation logic for Versioneer. For Git, this means creating/changing .gitattributes to mark _version.py @@ -1149,36 +1434,40 @@ def do_vcs_install(manifest_in, versionfile_source, ipy): GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - files = [manifest_in, versionfile_source] + files = [versionfile_source] if ipy: files.append(ipy) - try: - me = __file__ - if me.endswith(".pyc") or me.endswith(".pyo"): - me = os.path.splitext(me)[0] + ".py" - versioneer_file = os.path.relpath(me) - except NameError: - versioneer_file = "versioneer.py" - files.append(versioneer_file) + if "VERSIONEER_PEP518" not in globals(): + try: + my_path = __file__ + if my_path.endswith((".pyc", ".pyo")): + my_path = os.path.splitext(my_path)[0] + ".py" + versioneer_file = os.path.relpath(my_path) + except NameError: + versioneer_file = "versioneer.py" + files.append(versioneer_file) present = False try: - f = open(".gitattributes", "r") - for line in f.readlines(): - if line.strip().startswith(versionfile_source): - if "export-subst" in line.strip().split()[1:]: - present = True - f.close() - except EnvironmentError: + with open(".gitattributes", "r") as fobj: + for line in fobj: + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + break + except OSError: pass if not present: - f = open(".gitattributes", "a+") - f.write("%s export-subst\n" % versionfile_source) - f.close() + with open(".gitattributes", "a+") as fobj: + fobj.write(f"{versionfile_source} export-subst\n") files.append(".gitattributes") run_command(GITS, ["add", "--"] + files) -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -1187,30 +1476,23 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): - return { - "version": dirname[len(parentdir_prefix) :], - "full-revisionid": None, - "dirty": False, - "error": None, - "date": None, - } - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: - print( - "Tried directories %s but none started with prefix %s" - % (str(rootdirs), parentdir_prefix) - ) + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.18) from +# This file was generated by 'versioneer.py' (0.29) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. @@ -1227,43 +1509,41 @@ def get_versions(): """ -def versions_from_file(filename): +def versions_from_file(filename: str) -> Dict[str, Any]: """Try to determine the version from _version.py if present.""" try: with open(filename) as f: contents = f.read() - except EnvironmentError: + except OSError: raise NotThisMethod("unable to read _version.py") - mo = re.search( - r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S - ) + mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) if not mo: - mo = re.search( - r"version_json = '''\r\n(.*)''' # END VERSION_JSON", contents, re.M | re.S - ) + mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) if not mo: raise NotThisMethod("no version_json in _version.py") return json.loads(mo.group(1)) -def write_to_version_file(filename, versions): +def write_to_version_file(filename: str, versions: Dict[str, Any]) -> None: """Write the given version number to the given _version.py file.""" - os.unlink(filename) - contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) + contents = json.dumps(versions, sort_keys=True, + indent=1, separators=(",", ": ")) with open(filename, "w") as f: f.write(SHORT_VERSION_PY % contents) print("set %s to '%s'" % (filename, versions["version"])) -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -1281,29 +1561,78 @@ def render_pep440(pieces): rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] + rendered = "0.post0.dev%d" % pieces["distance"] return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -1330,12 +1659,41 @@ def render_pep440_post(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -1352,7 +1710,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -1372,7 +1730,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -1392,26 +1750,28 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: - return { - "version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None, - } + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": @@ -1421,20 +1781,16 @@ def render(pieces, style): else: raise ValueError("unknown style '%s'" % style) - return { - "version": rendered, - "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], - "error": None, - "date": pieces.get("date"), - } + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} class VersioneerBadRootError(Exception): """The project root directory is unknown or missing key files.""" -def get_versions(verbose=False): +def get_versions(verbose: bool = False) -> Dict[str, Any]: """Get the project version from whatever source is available. Returns dict with two keys: 'version' and 'full'. @@ -1449,10 +1805,9 @@ def get_versions(verbose=False): assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" handlers = HANDLERS.get(cfg.VCS) assert handlers, "unrecognized VCS '%s'" % cfg.VCS - verbose = verbose or cfg.verbose - assert ( - cfg.versionfile_source is not None - ), "please set versioneer.versionfile_source" + verbose = verbose or bool(cfg.verbose) # `bool()` used to avoid `None` + assert cfg.versionfile_source is not None, \ + "please set versioneer.versionfile_source" assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" versionfile_abs = os.path.join(root, cfg.versionfile_source) @@ -1506,22 +1861,22 @@ def get_versions(verbose=False): if verbose: print("unable to compute version") - return { - "version": "0+unknown", - "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", - "date": None, - } + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, "error": "unable to compute version", + "date": None} -def get_version(): +def get_version() -> str: """Get the short version string for this project.""" return get_versions()["version"] -def get_cmdclass(): - """Get the custom setuptools/distutils subclasses used by Versioneer.""" +def get_cmdclass(cmdclass: Optional[Dict[str, Any]] = None): + """Get the custom setuptools subclasses used by Versioneer. + + If the package uses a different cmdclass (e.g. one from numpy), it + should be provide as an argument. + """ if "versioneer" in sys.modules: del sys.modules["versioneer"] # this fixes the "python setup.py develop" case (also 'install' and @@ -1535,25 +1890,25 @@ def get_cmdclass(): # parent is protected against the child's "import versioneer". By # removing ourselves from sys.modules here, before the child build # happens, we protect the child from the parent's versioneer too. - # Also see https://github.com/warner/python-versioneer/issues/52 + # Also see https://github.com/python-versioneer/python-versioneer/issues/52 - cmds = {} + cmds = {} if cmdclass is None else cmdclass.copy() - # we add "version" to both distutils and setuptools - from distutils.core import Command + # we add "version" to setuptools + from setuptools import Command class cmd_version(Command): description = "report generated version string" - user_options = [] - boolean_options = [] + user_options: List[Tuple[str, str, str]] = [] + boolean_options: List[str] = [] - def initialize_options(self): + def initialize_options(self) -> None: pass - def finalize_options(self): + def finalize_options(self) -> None: pass - def run(self): + def run(self) -> None: vers = get_versions(verbose=True) print("Version: %s" % vers["version"]) print(" full-revisionid: %s" % vers.get("full-revisionid")) @@ -1561,10 +1916,9 @@ def run(self): print(" date: %s" % vers.get("date")) if vers["error"]: print(" error: %s" % vers["error"]) - cmds["version"] = cmd_version - # we override "build_py" in both distutils and setuptools + # we override "build_py" in setuptools # # most invocation pathways end up running build_py: # distutils/build -> build_py @@ -1579,30 +1933,68 @@ def run(self): # then does setup.py bdist_wheel, or sometimes setup.py install # setup.py egg_info -> ? + # pip install -e . and setuptool/editable_wheel will invoke build_py + # but the build_py command is not expected to copy any files. + # we override different "build_py" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.build_py import build_py as _build_py + if 'build_py' in cmds: + _build_py: Any = cmds['build_py'] else: - from distutils.command.build_py import build_py as _build_py + from setuptools.command.build_py import build_py as _build_py class cmd_build_py(_build_py): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() _build_py.run(self) + if getattr(self, "editable_mode", False): + # During editable installs `.py` and data files are + # not copied to build_lib + return # now locate _version.py in the new build/ directory and replace # it with an updated value if cfg.versionfile_build: - target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) - cmds["build_py"] = cmd_build_py - if "cx_Freeze" in sys.modules: # cx_freeze enabled? - from cx_Freeze.dist import build_exe as _build_exe + if 'build_ext' in cmds: + _build_ext: Any = cmds['build_ext'] + else: + from setuptools.command.build_ext import build_ext as _build_ext + + class cmd_build_ext(_build_ext): + def run(self) -> None: + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + _build_ext.run(self) + if self.inplace: + # build_ext --inplace will only build extensions in + # build/lib<..> dir with no _version.py to write to. + # As in place builds will already have a _version.py + # in the module dir, we do not need to write one. + return + # now locate _version.py in the new build/ directory and replace + # it with an updated value + if not cfg.versionfile_build: + return + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) + if not os.path.exists(target_versionfile): + print(f"Warning: {target_versionfile} does not exist, skipping " + "version update. This can happen if you are running build_ext " + "without first running build_py.") + return + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + cmds["build_ext"] = cmd_build_ext + if "cx_Freeze" in sys.modules: # cx_freeze enabled? + from cx_Freeze.dist import build_exe as _build_exe # type: ignore # nczeczulin reports that py2exe won't like the pep440-style string # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. # setup(console=[{ @@ -1611,7 +2003,7 @@ def run(self): # ... class cmd_build_exe(_build_exe): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() @@ -1623,28 +2015,24 @@ def run(self): os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write( - LONG - % { - "DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - } - ) - + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) cmds["build_exe"] = cmd_build_exe del cmds["build_py"] - if "py2exe" in sys.modules: # py2exe enabled? + if 'py2exe' in sys.modules: # py2exe enabled? try: - from py2exe.distutils_buildexe import py2exe as _py2exe # py3 + from py2exe.setuptools_buildexe import py2exe as _py2exe # type: ignore except ImportError: - from py2exe.build_exe import py2exe as _py2exe # py2 + from py2exe.distutils_buildexe import py2exe as _py2exe # type: ignore class cmd_py2exe(_py2exe): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() @@ -1656,27 +2044,60 @@ def run(self): os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write( - LONG - % { - "DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - } - ) - + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) cmds["py2exe"] = cmd_py2exe + # sdist farms its file list building out to egg_info + if 'egg_info' in cmds: + _egg_info: Any = cmds['egg_info'] + else: + from setuptools.command.egg_info import egg_info as _egg_info + + class cmd_egg_info(_egg_info): + def find_sources(self) -> None: + # egg_info.find_sources builds the manifest list and writes it + # in one shot + super().find_sources() + + # Modify the filelist and normalize it + root = get_root() + cfg = get_config_from_root(root) + self.filelist.append('versioneer.py') + if cfg.versionfile_source: + # There are rare cases where versionfile_source might not be + # included by default, so we must be explicit + self.filelist.append(cfg.versionfile_source) + self.filelist.sort() + self.filelist.remove_duplicates() + + # The write method is hidden in the manifest_maker instance that + # generated the filelist and was thrown away + # We will instead replicate their final normalization (to unicode, + # and POSIX-style paths) + from setuptools import unicode_utils + normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/') + for f in self.filelist.files] + + manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt') + with open(manifest_filename, 'w') as fobj: + fobj.write('\n'.join(normalized)) + + cmds['egg_info'] = cmd_egg_info + # we override different "sdist" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.sdist import sdist as _sdist + if 'sdist' in cmds: + _sdist: Any = cmds['sdist'] else: - from distutils.command.sdist import sdist as _sdist + from setuptools.command.sdist import sdist as _sdist class cmd_sdist(_sdist): - def run(self): + def run(self) -> None: versions = get_versions() self._versioneer_generated_versions = versions # unless we update this, the command will keep using the old @@ -1684,7 +2105,7 @@ def run(self): self.distribution.metadata.version = versions["version"] return _sdist.run(self) - def make_release_tree(self, base_dir, files): + def make_release_tree(self, base_dir: str, files: List[str]) -> None: root = get_root() cfg = get_config_from_root(root) _sdist.make_release_tree(self, base_dir, files) @@ -1693,10 +2114,8 @@ def make_release_tree(self, base_dir, files): # updated value target_versionfile = os.path.join(base_dir, cfg.versionfile_source) print("UPDATING %s" % target_versionfile) - write_to_version_file( - target_versionfile, self._versioneer_generated_versions - ) - + write_to_version_file(target_versionfile, + self._versioneer_generated_versions) cmds["sdist"] = cmd_sdist return cmds @@ -1739,25 +2158,28 @@ def make_release_tree(self, base_dir, files): """ -INIT_PY_SNIPPET = """ +OLD_SNIPPET = """ from ._version import get_versions __version__ = get_versions()['version'] del get_versions """ +INIT_PY_SNIPPET = """ +from . import {0} +__version__ = {0}.get_versions()['version'] +""" + -def do_setup(): - """Main VCS-independent setup function for installing Versioneer.""" +def do_setup() -> int: + """Do main VCS-independent setup function for installing Versioneer.""" root = get_root() try: cfg = get_config_from_root(root) - except ( - EnvironmentError, - configparser.NoSectionError, - configparser.NoOptionError, - ) as e: - if isinstance(e, (EnvironmentError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", file=sys.stderr) + except (OSError, configparser.NoSectionError, + configparser.NoOptionError) as e: + if isinstance(e, (OSError, configparser.NoSectionError)): + print("Adding sample versioneer config to setup.cfg", + file=sys.stderr) with open(os.path.join(root, "setup.cfg"), "a") as f: f.write(SAMPLE_CONFIG) print(CONFIG_ERROR, file=sys.stderr) @@ -1766,76 +2188,46 @@ def do_setup(): print(" creating %s" % cfg.versionfile_source) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write( - LONG - % { - "DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - } - ) - - ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") + f.write(LONG % {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + + ipy = os.path.join(os.path.dirname(cfg.versionfile_source), + "__init__.py") + maybe_ipy: Optional[str] = ipy if os.path.exists(ipy): try: with open(ipy, "r") as f: old = f.read() - except EnvironmentError: + except OSError: old = "" - if INIT_PY_SNIPPET not in old: + module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] + snippet = INIT_PY_SNIPPET.format(module) + if OLD_SNIPPET in old: + print(" replacing boilerplate in %s" % ipy) + with open(ipy, "w") as f: + f.write(old.replace(OLD_SNIPPET, snippet)) + elif snippet not in old: print(" appending to %s" % ipy) with open(ipy, "a") as f: - f.write(INIT_PY_SNIPPET) + f.write(snippet) else: print(" %s unmodified" % ipy) else: print(" %s doesn't exist, ok" % ipy) - ipy = None - - # Make sure both the top-level "versioneer.py" and versionfile_source - # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so - # they'll be copied into source distributions. Pip won't be able to - # install the package without this. - manifest_in = os.path.join(root, "MANIFEST.in") - simple_includes = set() - try: - with open(manifest_in, "r") as f: - for line in f: - if line.startswith("include "): - for include in line.split()[1:]: - simple_includes.add(include) - except EnvironmentError: - pass - # That doesn't cover everything MANIFEST.in can do - # (http://docs.python.org/2/distutils/sourcedist.html#commands), so - # it might give some false negatives. Appending redundant 'include' - # lines is safe, though. - if "versioneer.py" not in simple_includes: - print(" appending 'versioneer.py' to MANIFEST.in") - with open(manifest_in, "a") as f: - f.write("include versioneer.py\n") - else: - print(" 'versioneer.py' already in MANIFEST.in") - if cfg.versionfile_source not in simple_includes: - print( - " appending versionfile_source ('%s') to MANIFEST.in" - % cfg.versionfile_source - ) - with open(manifest_in, "a") as f: - f.write("include %s\n" % cfg.versionfile_source) - else: - print(" versionfile_source already in MANIFEST.in") + maybe_ipy = None # Make VCS-specific changes. For git, this means creating/changing # .gitattributes to mark _version.py for export-subst keyword # substitution. - do_vcs_install(manifest_in, cfg.versionfile_source, ipy) + do_vcs_install(cfg.versionfile_source, maybe_ipy) return 0 -def scan_setup_py(): +def scan_setup_py() -> int: """Validate the contents of setup.py against Versioneer's expectations.""" found = set() setters = False @@ -1872,10 +2264,14 @@ def scan_setup_py(): return errors +def setup_command() -> NoReturn: + """Set up Versioneer and exit with appropriate error code.""" + errors = do_setup() + errors += scan_setup_py() + sys.exit(1 if errors else 0) + + if __name__ == "__main__": cmd = sys.argv[1] if cmd == "setup": - errors = do_setup() - errors += scan_setup_py() - if errors: - sys.exit(1) + setup_command() From de627b969b7dfd9efba41c64200020adc78f81ec Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 9 Apr 2024 11:14:52 +0100 Subject: [PATCH 081/121] Remove duplicate __version__. --- python/BioSimSpace/__init__.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/python/BioSimSpace/__init__.py b/python/BioSimSpace/__init__.py index 62bbb7974..3b9b538cf 100644 --- a/python/BioSimSpace/__init__.py +++ b/python/BioSimSpace/__init__.py @@ -253,10 +253,6 @@ def _isVerbose(): from . import Types from . import Units -from ._version import get_versions - -__version__ = get_versions()["version"] -del get_versions - from . import _version -__version__ = _version.get_versions()['version'] +__version__ = _version.get_versions()["version"] +del _version From 9e5423725fdc62b4cdbc933edcead9014f935307 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 9 Apr 2024 11:26:19 +0100 Subject: [PATCH 082/121] Blacken. --- python/BioSimSpace/__init__.py | 1 + python/BioSimSpace/_version.py | 157 ++++++++++------- python/versioneer.py | 309 +++++++++++++++++++-------------- 3 files changed, 279 insertions(+), 188 deletions(-) diff --git a/python/BioSimSpace/__init__.py b/python/BioSimSpace/__init__.py index 3b9b538cf..fd823e8ad 100644 --- a/python/BioSimSpace/__init__.py +++ b/python/BioSimSpace/__init__.py @@ -254,5 +254,6 @@ def _isVerbose(): from . import Units from . import _version + __version__ = _version.get_versions()["version"] del _version diff --git a/python/BioSimSpace/_version.py b/python/BioSimSpace/_version.py index e6befd83e..7b3deef35 100644 --- a/python/BioSimSpace/_version.py +++ b/python/BioSimSpace/_version.py @@ -1,4 +1,3 @@ - # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build @@ -68,12 +67,14 @@ class NotThisMethod(Exception): def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f + return decorate @@ -100,10 +101,14 @@ def run_command( try: dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen([command] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None), **popen_kwargs) + process = subprocess.Popen( + [command] + args, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr else None), + **popen_kwargs, + ) break except OSError as e: if e.errno == errno.ENOENT: @@ -141,15 +146,21 @@ def versions_from_parentdir( for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} + return { + "version": dirname[len(parentdir_prefix) :], + "full-revisionid": None, + "dirty": False, + "error": None, + "date": None, + } rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) + print( + "Tried directories %s but none started with prefix %s" + % (str(rootdirs), parentdir_prefix) + ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -212,7 +223,7 @@ def git_versions_from_keywords( # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} + tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -221,7 +232,7 @@ def git_versions_from_keywords( # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = {r for r in refs if re.search(r'\d', r)} + tags = {r for r in refs if re.search(r"\d", r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -229,32 +240,36 @@ def git_versions_from_keywords( for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] + r = ref[len(tag_prefix) :] # Filter out refs that exactly match prefix or that don't start # with a number once the prefix is stripped (mostly a concern # when prefix is '') - if not re.match(r'\d', r): + if not re.match(r"\d", r): continue if verbose: print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} + return { + "version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": None, + "date": date, + } # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} + return { + "version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": "no suitable tags", + "date": None, + } @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs( - tag_prefix: str, - root: str, - verbose: bool, - runner: Callable = run_command + tag_prefix: str, root: str, verbose: bool, runner: Callable = run_command ) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. @@ -273,8 +288,7 @@ def git_pieces_from_vcs( env.pop("GIT_DIR", None) runner = functools.partial(runner, env=env) - _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=not verbose) + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -282,10 +296,19 @@ def git_pieces_from_vcs( # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = runner(GITS, [ - "describe", "--tags", "--dirty", "--always", "--long", - "--match", f"{tag_prefix}[[:digit:]]*" - ], cwd=root) + describe_out, rc = runner( + GITS, + [ + "describe", + "--tags", + "--dirty", + "--always", + "--long", + "--match", + f"{tag_prefix}[[:digit:]]*", + ], + cwd=root, + ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") @@ -300,8 +323,7 @@ def git_pieces_from_vcs( pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None - branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], - cwd=root) + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) # --abbrev-ref was added in git-1.6.3 if rc != 0 or branch_name is None: raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") @@ -341,17 +363,16 @@ def git_pieces_from_vcs( dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] + git_describe = git_describe[: git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag @@ -360,10 +381,12 @@ def git_pieces_from_vcs( if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) + pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( + full_tag, + tag_prefix, + ) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] + pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -412,8 +435,7 @@ def render_pep440(pieces: Dict[str, Any]) -> str: rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -442,8 +464,7 @@ def render_pep440_branch(pieces: Dict[str, Any]) -> str: rendered = "0" if pieces["branch"] != "master": rendered += ".dev0" - rendered += "+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -604,11 +625,13 @@ def render_git_describe_long(pieces: Dict[str, Any]) -> str: def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} + return { + "version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None, + } if not style or style == "default": style = "pep440" # the default @@ -632,9 +655,13 @@ def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: else: raise ValueError("unknown style '%s'" % style) - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} + return { + "version": rendered, + "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], + "error": None, + "date": pieces.get("date"), + } def get_versions() -> Dict[str, Any]: @@ -648,8 +675,7 @@ def get_versions() -> Dict[str, Any]: verbose = cfg.verbose try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass @@ -658,13 +684,16 @@ def get_versions() -> Dict[str, Any]: # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for _ in cfg.versionfile_source.split('/'): + for _ in cfg.versionfile_source.split("/"): root = os.path.dirname(root) except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None, + } try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) @@ -678,6 +707,10 @@ def get_versions() -> Dict[str, Any]: except NotThisMethod: pass - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", + "date": None, + } diff --git a/python/versioneer.py b/python/versioneer.py index 1e3753e63..de97d9042 100644 --- a/python/versioneer.py +++ b/python/versioneer.py @@ -1,4 +1,3 @@ - # Version: 0.29 """The Versioneer - like a rocketeer, but for versions. @@ -367,11 +366,13 @@ def get_root() -> str: or os.path.exists(pyproject_toml) or os.path.exists(versioneer_py) ): - err = ("Versioneer was unable to run the project root directory. " - "Versioneer requires setup.py to be executed from " - "its immediate directory (like 'python setup.py COMMAND'), " - "or in a way that lets it use sys.argv[0] to find the root " - "(like 'python path/to/setup.py COMMAND').") + err = ( + "Versioneer was unable to run the project root directory. " + "Versioneer requires setup.py to be executed from " + "its immediate directory (like 'python setup.py COMMAND'), " + "or in a way that lets it use sys.argv[0] to find the root " + "(like 'python path/to/setup.py COMMAND')." + ) raise VersioneerBadRootError(err) try: # Certain runtime workflows (setup.py install/develop in a setuptools @@ -384,8 +385,10 @@ def get_root() -> str: me_dir = os.path.normcase(os.path.splitext(my_path)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): - print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(my_path), versioneer_py)) + print( + "Warning: build in %s is using versioneer.py from %s" + % (os.path.dirname(my_path), versioneer_py) + ) except NameError: pass return root @@ -403,9 +406,9 @@ def get_config_from_root(root: str) -> VersioneerConfig: section: Union[Dict[str, Any], configparser.SectionProxy, None] = None if pyproject_toml.exists() and have_tomllib: try: - with open(pyproject_toml, 'rb') as fobj: + with open(pyproject_toml, "rb") as fobj: pp = tomllib.load(fobj) - section = pp['tool']['versioneer'] + section = pp["tool"]["versioneer"] except (tomllib.TOMLDecodeError, KeyError) as e: print(f"Failed to load config from {pyproject_toml}: {e}") print("Try to load it from setup.cfg") @@ -422,7 +425,7 @@ def get_config_from_root(root: str) -> VersioneerConfig: # `None` values elsewhere where it matters cfg = VersioneerConfig() - cfg.VCS = section['VCS'] + cfg.VCS = section["VCS"] cfg.style = section.get("style", "") cfg.versionfile_source = cast(str, section.get("versionfile_source")) cfg.versionfile_build = section.get("versionfile_build") @@ -450,10 +453,12 @@ class NotThisMethod(Exception): def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" HANDLERS.setdefault(vcs, {})[method] = f return f + return decorate @@ -480,10 +485,14 @@ def run_command( try: dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen([command] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None), **popen_kwargs) + process = subprocess.Popen( + [command] + args, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr else None), + **popen_kwargs, + ) break except OSError as e: if e.errno == errno.ENOENT: @@ -505,7 +514,9 @@ def run_command( return stdout, process.returncode -LONG_VERSION_PY['git'] = r''' +LONG_VERSION_PY[ + "git" +] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build @@ -1250,7 +1261,7 @@ def git_versions_from_keywords( # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} + tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -1259,7 +1270,7 @@ def git_versions_from_keywords( # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = {r for r in refs if re.search(r'\d', r)} + tags = {r for r in refs if re.search(r"\d", r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -1267,32 +1278,36 @@ def git_versions_from_keywords( for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] + r = ref[len(tag_prefix) :] # Filter out refs that exactly match prefix or that don't start # with a number once the prefix is stripped (mostly a concern # when prefix is '') - if not re.match(r'\d', r): + if not re.match(r"\d", r): continue if verbose: print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} + return { + "version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": None, + "date": date, + } # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} + return { + "version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": "no suitable tags", + "date": None, + } @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs( - tag_prefix: str, - root: str, - verbose: bool, - runner: Callable = run_command + tag_prefix: str, root: str, verbose: bool, runner: Callable = run_command ) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. @@ -1311,8 +1326,7 @@ def git_pieces_from_vcs( env.pop("GIT_DIR", None) runner = functools.partial(runner, env=env) - _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=not verbose) + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -1320,10 +1334,19 @@ def git_pieces_from_vcs( # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = runner(GITS, [ - "describe", "--tags", "--dirty", "--always", "--long", - "--match", f"{tag_prefix}[[:digit:]]*" - ], cwd=root) + describe_out, rc = runner( + GITS, + [ + "describe", + "--tags", + "--dirty", + "--always", + "--long", + "--match", + f"{tag_prefix}[[:digit:]]*", + ], + cwd=root, + ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") @@ -1338,8 +1361,7 @@ def git_pieces_from_vcs( pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None - branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], - cwd=root) + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) # --abbrev-ref was added in git-1.6.3 if rc != 0 or branch_name is None: raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") @@ -1379,17 +1401,16 @@ def git_pieces_from_vcs( dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] + git_describe = git_describe[: git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag @@ -1398,10 +1419,12 @@ def git_pieces_from_vcs( if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) + pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( + full_tag, + tag_prefix, + ) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] + pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -1479,15 +1502,21 @@ def versions_from_parentdir( for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} + return { + "version": dirname[len(parentdir_prefix) :], + "full-revisionid": None, + "dirty": False, + "error": None, + "date": None, + } rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) + print( + "Tried directories %s but none started with prefix %s" + % (str(rootdirs), parentdir_prefix) + ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -1516,11 +1545,13 @@ def versions_from_file(filename: str) -> Dict[str, Any]: contents = f.read() except OSError: raise NotThisMethod("unable to read _version.py") - mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) + mo = re.search( + r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S + ) if not mo: - mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) + mo = re.search( + r"version_json = '''\r\n(.*)''' # END VERSION_JSON", contents, re.M | re.S + ) if not mo: raise NotThisMethod("no version_json in _version.py") return json.loads(mo.group(1)) @@ -1528,8 +1559,7 @@ def versions_from_file(filename: str) -> Dict[str, Any]: def write_to_version_file(filename: str, versions: Dict[str, Any]) -> None: """Write the given version number to the given _version.py file.""" - contents = json.dumps(versions, sort_keys=True, - indent=1, separators=(",", ": ")) + contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) with open(filename, "w") as f: f.write(SHORT_VERSION_PY % contents) @@ -1561,8 +1591,7 @@ def render_pep440(pieces: Dict[str, Any]) -> str: rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -1591,8 +1620,7 @@ def render_pep440_branch(pieces: Dict[str, Any]) -> str: rendered = "0" if pieces["branch"] != "master": rendered += ".dev0" - rendered += "+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -1753,11 +1781,13 @@ def render_git_describe_long(pieces: Dict[str, Any]) -> str: def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} + return { + "version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None, + } if not style or style == "default": style = "pep440" # the default @@ -1781,9 +1811,13 @@ def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: else: raise ValueError("unknown style '%s'" % style) - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} + return { + "version": rendered, + "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], + "error": None, + "date": pieces.get("date"), + } class VersioneerBadRootError(Exception): @@ -1806,8 +1840,9 @@ def get_versions(verbose: bool = False) -> Dict[str, Any]: handlers = HANDLERS.get(cfg.VCS) assert handlers, "unrecognized VCS '%s'" % cfg.VCS verbose = verbose or bool(cfg.verbose) # `bool()` used to avoid `None` - assert cfg.versionfile_source is not None, \ - "please set versioneer.versionfile_source" + assert ( + cfg.versionfile_source is not None + ), "please set versioneer.versionfile_source" assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" versionfile_abs = os.path.join(root, cfg.versionfile_source) @@ -1861,9 +1896,13 @@ def get_versions(verbose: bool = False) -> Dict[str, Any]: if verbose: print("unable to compute version") - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, "error": "unable to compute version", - "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", + "date": None, + } def get_version() -> str: @@ -1916,6 +1955,7 @@ def run(self) -> None: print(" date: %s" % vers.get("date")) if vers["error"]: print(" error: %s" % vers["error"]) + cmds["version"] = cmd_version # we override "build_py" in setuptools @@ -1937,8 +1977,8 @@ def run(self) -> None: # but the build_py command is not expected to copy any files. # we override different "build_py" commands for both environments - if 'build_py' in cmds: - _build_py: Any = cmds['build_py'] + if "build_py" in cmds: + _build_py: Any = cmds["build_py"] else: from setuptools.command.build_py import build_py as _build_py @@ -1955,14 +1995,14 @@ def run(self) -> None: # now locate _version.py in the new build/ directory and replace # it with an updated value if cfg.versionfile_build: - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) + target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) + cmds["build_py"] = cmd_build_py - if 'build_ext' in cmds: - _build_ext: Any = cmds['build_ext'] + if "build_ext" in cmds: + _build_ext: Any = cmds["build_ext"] else: from setuptools.command.build_ext import build_ext as _build_ext @@ -1982,19 +2022,22 @@ def run(self) -> None: # it with an updated value if not cfg.versionfile_build: return - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) + target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) if not os.path.exists(target_versionfile): - print(f"Warning: {target_versionfile} does not exist, skipping " - "version update. This can happen if you are running build_ext " - "without first running build_py.") + print( + f"Warning: {target_versionfile} does not exist, skipping " + "version update. This can happen if you are running build_ext " + "without first running build_py." + ) return print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) + cmds["build_ext"] = cmd_build_ext if "cx_Freeze" in sys.modules: # cx_freeze enabled? from cx_Freeze.dist import build_exe as _build_exe # type: ignore + # nczeczulin reports that py2exe won't like the pep440-style string # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. # setup(console=[{ @@ -2015,17 +2058,21 @@ def run(self) -> None: os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) + f.write( + LONG + % { + "DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + } + ) + cmds["build_exe"] = cmd_build_exe del cmds["build_py"] - if 'py2exe' in sys.modules: # py2exe enabled? + if "py2exe" in sys.modules: # py2exe enabled? try: from py2exe.setuptools_buildexe import py2exe as _py2exe # type: ignore except ImportError: @@ -2044,18 +2091,22 @@ def run(self) -> None: os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) + f.write( + LONG + % { + "DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + } + ) + cmds["py2exe"] = cmd_py2exe # sdist farms its file list building out to egg_info - if 'egg_info' in cmds: - _egg_info: Any = cmds['egg_info'] + if "egg_info" in cmds: + _egg_info: Any = cmds["egg_info"] else: from setuptools.command.egg_info import egg_info as _egg_info @@ -2068,7 +2119,7 @@ def find_sources(self) -> None: # Modify the filelist and normalize it root = get_root() cfg = get_config_from_root(root) - self.filelist.append('versioneer.py') + self.filelist.append("versioneer.py") if cfg.versionfile_source: # There are rare cases where versionfile_source might not be # included by default, so we must be explicit @@ -2081,18 +2132,21 @@ def find_sources(self) -> None: # We will instead replicate their final normalization (to unicode, # and POSIX-style paths) from setuptools import unicode_utils - normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/') - for f in self.filelist.files] - manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt') - with open(manifest_filename, 'w') as fobj: - fobj.write('\n'.join(normalized)) + normalized = [ + unicode_utils.filesys_decode(f).replace(os.sep, "/") + for f in self.filelist.files + ] + + manifest_filename = os.path.join(self.egg_info, "SOURCES.txt") + with open(manifest_filename, "w") as fobj: + fobj.write("\n".join(normalized)) - cmds['egg_info'] = cmd_egg_info + cmds["egg_info"] = cmd_egg_info # we override different "sdist" commands for both environments - if 'sdist' in cmds: - _sdist: Any = cmds['sdist'] + if "sdist" in cmds: + _sdist: Any = cmds["sdist"] else: from setuptools.command.sdist import sdist as _sdist @@ -2114,8 +2168,10 @@ def make_release_tree(self, base_dir: str, files: List[str]) -> None: # updated value target_versionfile = os.path.join(base_dir, cfg.versionfile_source) print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, - self._versioneer_generated_versions) + write_to_version_file( + target_versionfile, self._versioneer_generated_versions + ) + cmds["sdist"] = cmd_sdist return cmds @@ -2175,11 +2231,9 @@ def do_setup() -> int: root = get_root() try: cfg = get_config_from_root(root) - except (OSError, configparser.NoSectionError, - configparser.NoOptionError) as e: + except (OSError, configparser.NoSectionError, configparser.NoOptionError) as e: if isinstance(e, (OSError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", - file=sys.stderr) + print("Adding sample versioneer config to setup.cfg", file=sys.stderr) with open(os.path.join(root, "setup.cfg"), "a") as f: f.write(SAMPLE_CONFIG) print(CONFIG_ERROR, file=sys.stderr) @@ -2188,15 +2242,18 @@ def do_setup() -> int: print(" creating %s" % cfg.versionfile_source) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - - ipy = os.path.join(os.path.dirname(cfg.versionfile_source), - "__init__.py") + f.write( + LONG + % { + "DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + } + ) + + ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") maybe_ipy: Optional[str] = ipy if os.path.exists(ipy): try: From 692849e2eee68b727bc19b2d7da685681fc9165e Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 9 Apr 2024 13:07:15 +0100 Subject: [PATCH 083/121] Use list rather than set so search strings are reproducible. [closes #270] --- .../Sandpit/Exscientia/_SireWrappers/_utils.py | 12 ++++++------ python/BioSimSpace/_SireWrappers/_utils.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_utils.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_utils.py index 6d735cc65..9f2b3eac3 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_utils.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_utils.py @@ -22,8 +22,8 @@ Utilities. """ -# A set of protein residues. Taken from MDAnalysis. -_prot_res = { +# A list of protein residues. Taken from MDAnalysis. +_prot_res = [ # CHARMM top_all27_prot_lipid.rtf "ALA", "ARG", @@ -135,10 +135,10 @@ "CMET", "CME", "ASF", -} +] -# A set of nucleic acid residues. Taken from MDAnalysis. -_nucl_res = { +# A list of nucleic acid residues. Taken from MDAnalysis. +_nucl_res = [ "ADE", "URA", "CYT", @@ -173,7 +173,7 @@ "RU3", "RG3", "RC3", -} +] # A list of ion elements. _ions = [ diff --git a/python/BioSimSpace/_SireWrappers/_utils.py b/python/BioSimSpace/_SireWrappers/_utils.py index 6d735cc65..9f2b3eac3 100644 --- a/python/BioSimSpace/_SireWrappers/_utils.py +++ b/python/BioSimSpace/_SireWrappers/_utils.py @@ -22,8 +22,8 @@ Utilities. """ -# A set of protein residues. Taken from MDAnalysis. -_prot_res = { +# A list of protein residues. Taken from MDAnalysis. +_prot_res = [ # CHARMM top_all27_prot_lipid.rtf "ALA", "ARG", @@ -135,10 +135,10 @@ "CMET", "CME", "ASF", -} +] -# A set of nucleic acid residues. Taken from MDAnalysis. -_nucl_res = { +# A list of nucleic acid residues. Taken from MDAnalysis. +_nucl_res = [ "ADE", "URA", "CYT", @@ -173,7 +173,7 @@ "RU3", "RG3", "RC3", -} +] # A list of ion elements. _ions = [ From 81d82338953c60a787dceb71b991e7c5aa602304 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 9 Apr 2024 14:50:48 +0100 Subject: [PATCH 084/121] Join protein and nucleic acid residue strings correctly. --- python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py | 2 ++ python/BioSimSpace/_SireWrappers/_system.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py index 56bf3aebe..7dc882868 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py @@ -1888,6 +1888,7 @@ def getRestraintAtoms( string = ( "(not water) and (resname " + ",".join(_prot_res) + + "," + ",".join(_nucl_res) + ") and (atomname N,CA,C,O,P,/C5'/,/C3'/,/O3'/,/O5'/)" ) @@ -1943,6 +1944,7 @@ def getRestraintAtoms( string = ( "(not water) and (resname " + ",".join(_prot_res) + + "," + ",".join(_nucl_res) + ") and (atomname N,CA,C,O,P,/C5'/,/C3'/,/O3'/,/O5'/)" ) diff --git a/python/BioSimSpace/_SireWrappers/_system.py b/python/BioSimSpace/_SireWrappers/_system.py index 380a3b736..2d458890b 100644 --- a/python/BioSimSpace/_SireWrappers/_system.py +++ b/python/BioSimSpace/_SireWrappers/_system.py @@ -1809,6 +1809,7 @@ def getRestraintAtoms( string = ( "(not water) and (resname " + ",".join(_prot_res) + + "," + ",".join(_nucl_res) + ") and (atomname N,CA,C,O,P,/C5'/,/C3'/,/O3'/,/O5'/)" ) @@ -1864,6 +1865,7 @@ def getRestraintAtoms( string = ( "(not water) and (resname " + ",".join(_prot_res) + + "," + ",".join(_nucl_res) + ") and (atomname N,CA,C,O,P,/C5'/,/C3'/,/O3'/,/O5'/)" ) From f44b35ea0b649f0a9a5910bb097fcf3cf1f78abc Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Tue, 9 Apr 2024 15:15:48 +0100 Subject: [PATCH 085/121] Use try/except when matching by coordinates. --- .../Sandpit/Exscientia/_SireWrappers/_molecule.py | 12 ++++++++++-- python/BioSimSpace/_SireWrappers/_molecule.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py index 6de6095dd..b4fa03ab7 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_molecule.py @@ -792,7 +792,11 @@ def makeCompatibleWith( if len(matches) < num_atoms0: # Atom names or order might have changed. Try to match by coordinates. matcher = _SireMol.AtomCoordMatcher() - matches = matcher.match(mol0, mol1) + + try: + matches = matcher.match(mol0, mol1) + except: + matches = [] # We need to rename the atoms. is_renamed = True @@ -1003,7 +1007,11 @@ def makeCompatibleWith( matcher = _SireMol.AtomCoordMatcher() # Get the matches for this molecule and append to the list. - match = matcher.match(mol0, mol) + try: + match = matcher.match(mol0, mol) + except: + match = [] + matches.append(match) num_matches += len(match) diff --git a/python/BioSimSpace/_SireWrappers/_molecule.py b/python/BioSimSpace/_SireWrappers/_molecule.py index 089c88146..27fa121f0 100644 --- a/python/BioSimSpace/_SireWrappers/_molecule.py +++ b/python/BioSimSpace/_SireWrappers/_molecule.py @@ -748,7 +748,11 @@ def makeCompatibleWith( if len(matches) < num_atoms0: # Atom names or order might have changed. Try to match by coordinates. matcher = _SireMol.AtomCoordMatcher() - matches = matcher.match(mol0, mol1) + + try: + matches = matcher.match(mol0, mol1) + except: + matches = [] # We need to rename the atoms. is_renamed = True @@ -959,7 +963,11 @@ def makeCompatibleWith( matcher = _SireMol.AtomCoordMatcher() # Get the matches for this molecule and append to the list. - match = matcher.match(mol0, mol) + try: + match = matcher.match(mol0, mol) + except: + match = [] + matches.append(match) num_matches += len(match) From 9b870cd9f2aa3fd735bb17032e068346ec19579a Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 11:19:57 +0100 Subject: [PATCH 086/121] Exclude sander from free-energy simulations. --- python/BioSimSpace/Process/_amber.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index 9ca469071..7a105a582 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -2816,7 +2816,10 @@ def _find_exe(is_gpu=False, is_free_energy=False, is_vacuum=False): if is_gpu: targets = ["pmemd.cuda"] else: - targets = ["pmemd", "sander"] + if is_free_energy: + targets = ["pmemd"] + else: + targets = ["pmemd", "sander"] # Search for the executable. From 11f59ffaaffb476b363ab87fdd8a07a38da19db8 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 11:34:49 +0100 Subject: [PATCH 087/121] Update to black 24. --- .../FreeEnergy/_restraint_search.py | 6 +- .../Sandpit/Exscientia/Protocol/_config.py | 110 +++++++++--------- python/BioSimSpace/_Config/_amber.py | 6 +- python/BioSimSpace/_Config/_gromacs.py | 6 +- recipes/biosimspace/template.yaml | 2 +- 5 files changed, 65 insertions(+), 65 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py index f7ec62dd4..31a53e956 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py +++ b/python/BioSimSpace/Sandpit/Exscientia/FreeEnergy/_restraint_search.py @@ -2145,9 +2145,9 @@ def _getRestraintDict(u, pairs_ordered): } # If this is the first pair, add it as the permanent distance restraint. if i == 0: - restraint_dict[ - "permanent_distance_restraint" - ] = individual_restraint_dict + restraint_dict["permanent_distance_restraint"] = ( + individual_restraint_dict + ) else: restraint_dict["distance_restraints"].append( individual_restraint_dict diff --git a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py index a08935d9b..5b9f53f6f 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Protocol/_config.py @@ -217,9 +217,9 @@ def generateAmberConfig(self, extra_options=None, extra_lines=None): protocol_dict["imin"] = 1 # Minimisation simulation. protocol_dict["ntmin"] = 2 # Set the minimisation method to XMIN protocol_dict["maxcyc"] = self._steps # Set the number of steps. - protocol_dict[ - "ncyc" - ] = num_steep # Set the number of steepest descent steps. + protocol_dict["ncyc"] = ( + num_steep # Set the number of steepest descent steps. + ) # FIX need to remove and fix this, only for initial testing timestep = 0.004 else: @@ -318,9 +318,9 @@ def generateAmberConfig(self, extra_options=None, extra_lines=None): # Don't use barostat for vacuum simulations. if self._has_box and self._has_water: protocol_dict["ntp"] = 1 # Isotropic pressure scaling. - protocol_dict[ - "pres0" - ] = f"{self.protocol.getPressure().bar().value():.5f}" # Pressure in bar. + protocol_dict["pres0"] = ( + f"{self.protocol.getPressure().bar().value():.5f}" # Pressure in bar. + ) if isinstance(self.protocol, _Protocol.Equilibration): protocol_dict["barostat"] = 1 # Berendsen barostat. else: @@ -466,23 +466,23 @@ def generateGromacsConfig( protocol_dict["cutoff-scheme"] = "Verlet" # Use Verlet pair lists. if self._has_box and self._has_water: protocol_dict["ns-type"] = "grid" # Use a grid to search for neighbours. - protocol_dict[ - "nstlist" - ] = "20" # Rebuild neighbour list every 20 steps. Recommended in the manual for parallel simulations and/or non-bonded force calculation on the GPU. + protocol_dict["nstlist"] = ( + "20" # Rebuild neighbour list every 20 steps. Recommended in the manual for parallel simulations and/or non-bonded force calculation on the GPU. + ) protocol_dict["rlist"] = "0.8" # Set short-range cutoff. protocol_dict["rvdw"] = "0.8" # Set van der Waals cutoff. protocol_dict["rcoulomb"] = "0.8" # Set Coulomb cutoff. protocol_dict["coulombtype"] = "PME" # Fast smooth Particle-Mesh Ewald. - protocol_dict[ - "DispCorr" - ] = "EnerPres" # Dispersion corrections for energy and pressure. + protocol_dict["DispCorr"] = ( + "EnerPres" # Dispersion corrections for energy and pressure. + ) else: # Perform vacuum simulations by implementing pseudo-PBC conditions, # i.e. run calculation in a near-infinite box (333.3 nm). # c.f.: https://pubmed.ncbi.nlm.nih.gov/29678588 - protocol_dict[ - "nstlist" - ] = "1" # Single neighbour list (all particles interact). + protocol_dict["nstlist"] = ( + "1" # Single neighbour list (all particles interact). + ) protocol_dict["rlist"] = "333.3" # "Infinite" short-range cutoff. protocol_dict["rvdw"] = "333.3" # "Infinite" van der Waals cutoff. protocol_dict["rcoulomb"] = "333.3" # "Infinite" Coulomb cutoff. @@ -503,12 +503,12 @@ def generateGromacsConfig( # 4ps time constant for pressure coupling. # As the tau-p has to be 10 times larger than nstpcouple * dt (4 fs) protocol_dict["tau-p"] = 4 - protocol_dict[ - "ref-p" - ] = f"{self.protocol.getPressure().bar().value():.5f}" # Pressure in bar. - protocol_dict[ - "compressibility" - ] = "4.5e-5" # Compressibility of water. + protocol_dict["ref-p"] = ( + f"{self.protocol.getPressure().bar().value():.5f}" # Pressure in bar. + ) + protocol_dict["compressibility"] = ( + "4.5e-5" # Compressibility of water. + ) else: _warnings.warn( "Cannot use a barostat for a vacuum or non-periodic simulation" @@ -518,9 +518,9 @@ def generateGromacsConfig( if not isinstance(self.protocol, _Protocol.Minimisation): protocol_dict["integrator"] = "md" # leap-frog dynamics. protocol_dict["tcoupl"] = "v-rescale" - protocol_dict[ - "tc-grps" - ] = "system" # A single temperature group for the entire system. + protocol_dict["tc-grps"] = ( + "system" # A single temperature group for the entire system. + ) protocol_dict["tau-t"] = "{:.5f}".format( self.protocol.getTauT().picoseconds().value() ) # Collision frequency (ps). @@ -535,12 +535,12 @@ def generateGromacsConfig( timestep = self.protocol.getTimeStep().picoseconds().value() end_time = _math.floor(timestep * self._steps) - protocol_dict[ - "annealing" - ] = "single" # Single sequence of annealing points. - protocol_dict[ - "annealing-npoints" - ] = 2 # Two annealing points for "system" temperature group. + protocol_dict["annealing"] = ( + "single" # Single sequence of annealing points. + ) + protocol_dict["annealing-npoints"] = ( + 2 # Two annealing points for "system" temperature group. + ) # Linearly change temperature between start and end times. protocol_dict["annealing-time"] = "0 %d" % end_time @@ -609,20 +609,20 @@ def tranform(charge, LJ): "temperature", ]: if name in LambdaValues: - protocol_dict[ - "{:<20}".format("{}-lambdas".format(name)) - ] = " ".join( - list(map("{:.5f}".format, LambdaValues[name].to_list())) + protocol_dict["{:<20}".format("{}-lambdas".format(name))] = ( + " ".join( + list(map("{:.5f}".format, LambdaValues[name].to_list())) + ) ) - protocol_dict[ - "init-lambda-state" - ] = self.protocol.getLambdaIndex() # Current lambda value. - protocol_dict[ - "nstcalcenergy" - ] = self._report_interval # Calculate energies every report_interval steps. - protocol_dict[ - "nstdhdl" - ] = self._report_interval # Write gradients every report_interval steps. + protocol_dict["init-lambda-state"] = ( + self.protocol.getLambdaIndex() + ) # Current lambda value. + protocol_dict["nstcalcenergy"] = ( + self._report_interval + ) # Calculate energies every report_interval steps. + protocol_dict["nstdhdl"] = ( + self._report_interval + ) # Write gradients every report_interval steps. # Handle the combination of multiple distance restraints and perturbation type # of "release_restraint". In this case, the force constant of the "permanent" @@ -829,18 +829,18 @@ def generateSomdConfig( # Free energies. if isinstance(self.protocol, _Protocol._FreeEnergyMixin): if not isinstance(self.protocol, _Protocol.Minimisation): - protocol_dict[ - "constraint" - ] = "hbonds-notperturbed" # Handle hydrogen perturbations. - protocol_dict[ - "energy frequency" - ] = 250 # Write gradients every 250 steps. + protocol_dict["constraint"] = ( + "hbonds-notperturbed" # Handle hydrogen perturbations. + ) + protocol_dict["energy frequency"] = ( + 250 # Write gradients every 250 steps. + ) protocol = [str(x) for x in self.protocol.getLambdaValues()] protocol_dict["lambda array"] = ", ".join(protocol) - protocol_dict[ - "lambda_val" - ] = self.protocol.getLambda() # Current lambda value. + protocol_dict["lambda_val"] = ( + self.protocol.getLambda() + ) # Current lambda value. try: # RBFE res_num = ( @@ -857,9 +857,9 @@ def generateSomdConfig( .value() ) - protocol_dict[ - "perturbed residue number" - ] = res_num # Perturbed residue number. + protocol_dict["perturbed residue number"] = ( + res_num # Perturbed residue number. + ) # Put everything together in a line-by-line format. total_dict = {**protocol_dict, **extra_options} diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index 381401fdd..7551fb1b9 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -306,9 +306,9 @@ def createConfig( # Isotropic pressure scaling. protocol_dict["ntp"] = 1 # Pressure in bar. - protocol_dict[ - "pres0" - ] = f"{self._protocol.getPressure().bar().value():.5f}" + protocol_dict["pres0"] = ( + f"{self._protocol.getPressure().bar().value():.5f}" + ) if isinstance(self._protocol, _Protocol.Equilibration): # Berendsen barostat. protocol_dict["barostat"] = 1 diff --git a/python/BioSimSpace/_Config/_gromacs.py b/python/BioSimSpace/_Config/_gromacs.py index 6724f2904..1355b274c 100644 --- a/python/BioSimSpace/_Config/_gromacs.py +++ b/python/BioSimSpace/_Config/_gromacs.py @@ -199,9 +199,9 @@ def createConfig(self, version=None, extra_options={}, extra_lines=[]): # 1ps time constant for pressure coupling. protocol_dict["tau-p"] = 1 # Pressure in bar. - protocol_dict[ - "ref-p" - ] = f"{self._protocol.getPressure().bar().value():.5f}" + protocol_dict["ref-p"] = ( + f"{self._protocol.getPressure().bar().value():.5f}" + ) # Compressibility of water. protocol_dict["compressibility"] = "4.5e-5" else: diff --git a/recipes/biosimspace/template.yaml b/recipes/biosimspace/template.yaml index 4e3a7edeb..fd9f0a4c4 100644 --- a/recipes/biosimspace/template.yaml +++ b/recipes/biosimspace/template.yaml @@ -27,7 +27,7 @@ test: - SIRE_SILENT_PHONEHOME requires: - pytest <8 - - black 23 # [linux and x86_64 and py==312] + - black 24 # [linux and x86_64 and py==312] - pytest-black # [linux and x86_64 and py==312] - ambertools # [linux and x86_64] - gromacs # [linux and x86_64] From 83fe05fabf37221b5ffdb4382ff60883c3b16ad8 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 13:27:31 +0100 Subject: [PATCH 088/121] Add AMBER as an example engine in hydration free-energy tutorial. --- doc/source/tutorials/hydration_freenrg.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/source/tutorials/hydration_freenrg.rst b/doc/source/tutorials/hydration_freenrg.rst index 5ff767df8..2b9a416d6 100644 --- a/doc/source/tutorials/hydration_freenrg.rst +++ b/doc/source/tutorials/hydration_freenrg.rst @@ -167,6 +167,20 @@ Let's examine the directory for the :math:`{\lambda=0}` window of the free leg: gromacs.err gromacs.mdp gromacs.out.mdp gromacs.tpr gromacs.gro gromacs.out gromacs.top +Similarly, we can also set up the same simulations using AMBER: + +.. code-block:: python + + free_amber = BSS.FreeEnergy.Relative(solvated, protocol, engine="amber", work_dir="freenrg_amber/free") + vac_amber = BSS.FreeEnergy.Relative(merged.toSystem(), protocol, engine="amber", work_dir="freenrg_amber/vacuum") + +Let's examine the directory for the :math:`{\lambda=0}` window of the free leg: + +.. code-block:: bash + + $ ls freenrg_amber/free/lambda_0.0000 + amber.cfg amber.err amber.out amber.prm7 amber_ref.rst7 amber.rst7 + There you go! This tutorial has shown you how BioSimSpace can be used to easily set up everything that is needed for complex alchemical free energy simulations. Please visit the :data:`API documentation ` for further information. From 621b75aa2dae9a57b8bfae0e8f58bb08951b6005 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 14:06:49 +0100 Subject: [PATCH 089/121] Improve comment regarding single-point energy difference. --- tests/Process/test_amber.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Process/test_amber.py b/tests/Process/test_amber.py index 6c726264d..a02627f03 100644 --- a/tests/Process/test_amber.py +++ b/tests/Process/test_amber.py @@ -504,4 +504,6 @@ def test_pmemd_fep(solvated_perturbable_system): nrg_pmemd_cuda = process.getTotalEnergy().value() # Vaccum energies currently differ between pmemd and pmemd.cuda. + # pmemd appears to be wrong as the CUDA version is consistent with + # sander and the result of non-FEP simulation. assert not math.isclose(nrg_pmemd, nrg_pmemd_cuda, rel_tol=1e-4) From 2ec7ee7cfb07398424f326c86ccb6d36786429d9 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 14:29:06 +0100 Subject: [PATCH 090/121] Use readMolecules to read NAMD output since MoleculeParser segfaults. --- python/BioSimSpace/Process/_namd.py | 4 +--- python/BioSimSpace/Sandpit/Exscientia/Process/_namd.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/python/BioSimSpace/Process/_namd.py b/python/BioSimSpace/Process/_namd.py index c50165947..9811d26ab 100644 --- a/python/BioSimSpace/Process/_namd.py +++ b/python/BioSimSpace/Process/_namd.py @@ -774,9 +774,7 @@ def getSystem(self, block="AUTO"): is_lambda1 = False # Load the restart file. - new_system = _System( - _SireIO.MoleculeParser.read(files, self._property_map) - ) + new_system = _IO.readMolecules(files, property_map=self._property_map) # Create a copy of the existing system object. old_system = self._system.copy() diff --git a/python/BioSimSpace/Sandpit/Exscientia/Process/_namd.py b/python/BioSimSpace/Sandpit/Exscientia/Process/_namd.py index 314f31a92..ca02c05b9 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Process/_namd.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Process/_namd.py @@ -759,9 +759,7 @@ def getSystem(self, block="AUTO"): is_lambda1 = False # Load the restart file. - new_system = _System( - _SireIO.MoleculeParser.read(files, self._property_map) - ) + new_system = _IO.readMolecules(files, property_map=self._property_map) # Create a copy of the existing system object. old_system = self._system.copy() From 844fb1d462fc2db11411a2937740881779e228dd Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 15:31:41 +0100 Subject: [PATCH 091/121] Don't use SHAKE for minimisation. [ci skip] --- python/BioSimSpace/_Config/_amber.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index 7551fb1b9..e0ee8f2f3 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -545,7 +545,7 @@ def _generate_amber_fep_masks( ti1_indices = mcs1_indices + dummy1_indices # SHAKE should be used for timestep >= 2 fs. - if timestep is None or timestep >= 0.002: + if timestep is not None and timestep >= 0.002: no_shake_mask = "" else: no_shake_mask = _amber_mask_from_indices(ti0_indices + ti1_indices) From 6ded910223edfdc9ff5e4ff0bb3a313fde213b15 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 18:11:35 +0100 Subject: [PATCH 092/121] Figured out how to match vacuum energies. --- python/BioSimSpace/_Config/_amber.py | 2 ++ tests/Process/test_amber.py | 17 +++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index e0ee8f2f3..861fa5d72 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -558,6 +558,8 @@ def _generate_amber_fep_masks( "scmask2": f'"{_amber_mask_from_indices(dummy1_indices)}"', "tishake": 0 if is_pmemd_cuda else 1, "noshakemask": f'"{no_shake_mask}"', + "gti_add_sc": 1, + "gti_bat_sc": 1, } return option_dict diff --git a/tests/Process/test_amber.py b/tests/Process/test_amber.py index a02627f03..a6626047e 100644 --- a/tests/Process/test_amber.py +++ b/tests/Process/test_amber.py @@ -490,20 +490,25 @@ def test_pmemd_fep(solvated_perturbable_system): vac_system = solvated_perturbable_system[0].toSystem() # Compute the single-point energy using pmemd. - process = BSS.Process.Amber(vac_system, protocol, exe=f"{bin_dir}/pmemd") + process = BSS.Process.Amber( + vac_system, protocol, exe=f"{bin_dir}/pmemd", extra_options={"gti_bat_sc": 2} + ) process.start() process.wait() assert not process.isError() nrg_pmemd = process.getTotalEnergy().value() # Compute the single-point energy using pmemd.cuda. - process = BSS.Process.Amber(vac_system, protocol, exe=f"{bin_dir}/pmemd.cuda") + process = BSS.Process.Amber( + vac_system, + protocol, + exe=f"{bin_dir}/pmemd.cuda", + extra_options={"gti_bat_sc": 2}, + ) process.start() process.wait() assert not process.isError() nrg_pmemd_cuda = process.getTotalEnergy().value() - # Vaccum energies currently differ between pmemd and pmemd.cuda. - # pmemd appears to be wrong as the CUDA version is consistent with - # sander and the result of non-FEP simulation. - assert not math.isclose(nrg_pmemd, nrg_pmemd_cuda, rel_tol=1e-4) + # Check that the energies are the same. + assert math.isclose(nrg_pmemd, nrg_pmemd_cuda, rel_tol=1e-3) From 72948411f73df2906c9e31c1d9b38f01c02f187f Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 18:20:37 +0100 Subject: [PATCH 093/121] Use correct file for reference system. --- python/BioSimSpace/Process/_openmm.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/Process/_openmm.py b/python/BioSimSpace/Process/_openmm.py index 66c461495..2b7a45de8 100644 --- a/python/BioSimSpace/Process/_openmm.py +++ b/python/BioSimSpace/Process/_openmm.py @@ -248,6 +248,9 @@ def _setup(self): # Convert the water model topology so that it matches the AMBER naming convention. system._set_water_topology("AMBER", property_map=self._property_map) + self._reference_system._set_water_topology( + "AMBER", property_map=self._property_map + ) # Check for perturbable molecules and convert to the chosen end state. system = self._checkPerturbable(system) @@ -269,7 +272,12 @@ def _setup(self): if self._protocol.getRestraint() is not None: try: file = _os.path.splitext(self._ref_file)[0] - _IO.saveMolecules(file, system, "rst7", property_map=self._property_map) + _IO.saveMolecules( + file, + self._reference_system, + "rst7", + property_map=self._property_map, + ) except Exception as e: msg = "Failed to write reference system to 'RST7' format." if _isVerbose(): @@ -2174,7 +2182,7 @@ def _add_config_restraints(self): restrained_atoms = restraint self.addToConfig( - f"ref_prm = parmed.load_file('{self._top_file}', '{self._rst_file}')" + f"ref_prm = parmed.load_file('{self._top_file}', '{self._ref_file}')" ) # Get the force constant in units of kJ_per_mol/nanometer**2 From daad0e1c9329cb0f189ddd4b5b2f2e66cf4102d4 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 18:20:51 +0100 Subject: [PATCH 094/121] Convert water topology of reference system so naming matches. --- python/BioSimSpace/Process/_amber.py | 3 +++ python/BioSimSpace/Process/_gromacs.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index 7a105a582..466216e87 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -265,6 +265,9 @@ def _setup(self, **kwargs): # Convert the water model topology so that it matches the AMBER naming convention. system._set_water_topology("AMBER", property_map=self._property_map) + self._reference_system._set_water_topology( + "AMBER", property_map=self._property_map + ) # Create the squashed system. if isinstance(self._protocol, _FreeEnergyMixin): diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index ec6fb6dea..a35fc3531 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -287,6 +287,9 @@ def _setup(self, **kwargs): # Convert the water model topology so that it matches the GROMACS naming convention. system._set_water_topology("GROMACS", property_map=self._property_map) + self._reference_system._set_water_topology( + "GROMACS", property_map=self._property_map + ) # GRO87 file. file = _os.path.splitext(self._gro_file)[0] From d33004c7be504cf1858a93ed5c798eea281fbc33 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 19:15:41 +0100 Subject: [PATCH 095/121] Try debugging Windows error. --- tests/Process/test_openmm.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Process/test_openmm.py b/tests/Process/test_openmm.py index 7925bd91b..9f45d2fff 100644 --- a/tests/Process/test_openmm.py +++ b/tests/Process/test_openmm.py @@ -129,6 +129,9 @@ def run_process(system, protocol): # Wait for the process to end. process.wait() + print(process.stdout(1000)) + print(process.stderr(1000)) + # Make sure the process didn't error. assert not process.isError() From 59323f5629880c4bcddf90494fc5d359f59b18e6 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 20:18:06 +0100 Subject: [PATCH 096/121] Remove debugging statements. --- tests/Process/test_openmm.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Process/test_openmm.py b/tests/Process/test_openmm.py index 9f45d2fff..7925bd91b 100644 --- a/tests/Process/test_openmm.py +++ b/tests/Process/test_openmm.py @@ -129,9 +129,6 @@ def run_process(system, protocol): # Wait for the process to end. process.wait() - print(process.stdout(1000)) - print(process.stderr(1000)) - # Make sure the process didn't error. assert not process.isError() From 3951502184724cecfa2df18b4f418b8df6a7ac88 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Wed, 10 Apr 2024 20:19:06 +0100 Subject: [PATCH 097/121] Use relative file names in OpenMM Python script. --- python/BioSimSpace/Process/_openmm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/BioSimSpace/Process/_openmm.py b/python/BioSimSpace/Process/_openmm.py index 2b7a45de8..d11974d22 100644 --- a/python/BioSimSpace/Process/_openmm.py +++ b/python/BioSimSpace/Process/_openmm.py @@ -344,7 +344,7 @@ def _generate_config(self): "\n# We use ParmEd due to issues with the built in AmberPrmtopFile for certain triclinic spaces." ) self.addToConfig( - f"prm = parmed.load_file('{self._top_file}', '{self._rst_file}')" + f"prm = parmed.load_file('{self._name}.prm7', '{self._name}.rst7')" ) # Don't use a cut-off if this is a vacuum simulation or if box information @@ -413,7 +413,7 @@ def _generate_config(self): "\n# We use ParmEd due to issues with the built in AmberPrmtopFile for certain triclinic spaces." ) self.addToConfig( - f"prm = parmed.load_file('{self._top_file}', '{self._rst_file}')" + f"prm = parmed.load_file('{self._name}.prm7', '{self._name}.rst7')" ) # Don't use a cut-off if this is a vacuum simulation or if box information @@ -597,7 +597,7 @@ def _generate_config(self): "\n# We use ParmEd due to issues with the built in AmberPrmtopFile for certain triclinic spaces." ) self.addToConfig( - f"prm = parmed.load_file('{self._top_file}', '{self._rst_file}')" + f"prm = parmed.load_file('{self._name}.prm7', '{self._name}.rst7')" ) # Don't use a cut-off if this is a vacuum simulation or if box information @@ -796,7 +796,7 @@ def _generate_config(self): "\n# We use ParmEd due to issues with the built in AmberPrmtopFile for certain triclinic spaces." ) self.addToConfig( - f"prm = parmed.load_file('{self._top_file}', '{self._rst_file}')" + f"prm = parmed.load_file('{self._name}.prm7', '{self._name}.rst7')" ) # Don't use a cut-off if this is a vacuum simulation or if box information @@ -2182,7 +2182,7 @@ def _add_config_restraints(self): restrained_atoms = restraint self.addToConfig( - f"ref_prm = parmed.load_file('{self._top_file}', '{self._ref_file}')" + f"ref_prm = parmed.load_file('{self._name}.prm7', '{self._name}_ref.rst7')" ) # Get the force constant in units of kJ_per_mol/nanometer**2 From 4d6f6a8e8d2843d7d66ce2eb1a46279d7eac6452 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 09:34:30 +0100 Subject: [PATCH 098/121] Switch to using os.path.join. --- python/BioSimSpace/FreeEnergy/_relative.py | 60 ++++++------ .../Parameters/_Protocol/_amber.py | 94 +++++++++---------- .../Parameters/_Protocol/_openforcefield.py | 26 ++--- python/BioSimSpace/Process/_amber.py | 16 ++-- python/BioSimSpace/Process/_gromacs.py | 37 +++++--- python/BioSimSpace/Process/_namd.py | 41 ++++---- python/BioSimSpace/Process/_openmm.py | 30 +++--- python/BioSimSpace/Process/_plumed.py | 29 +++--- python/BioSimSpace/Process/_process.py | 14 +-- python/BioSimSpace/Process/_process_runner.py | 4 +- python/BioSimSpace/Process/_somd.py | 24 ++--- python/BioSimSpace/Trajectory/_trajectory.py | 24 +++-- 12 files changed, 221 insertions(+), 178 deletions(-) diff --git a/python/BioSimSpace/FreeEnergy/_relative.py b/python/BioSimSpace/FreeEnergy/_relative.py index a54a6d707..4e197f1dc 100644 --- a/python/BioSimSpace/FreeEnergy/_relative.py +++ b/python/BioSimSpace/FreeEnergy/_relative.py @@ -406,7 +406,7 @@ def getData(self, name="data", file_link=False, work_dir=None): ) # Write to the zip file. - with _zipfile.ZipFile(cwd + f"/{zipname}", "w") as zip: + with _zipfile.Zipfile(_os.join(cwd, zipname), "w") as zip: for file in files: zip.write(file) @@ -2074,15 +2074,15 @@ def _initialise_runner(self, system): process._system = first_process._system.copy() process._protocol = self._protocol process._work_dir = new_dir - process._std_out_file = new_dir + "/somd.out" - process._std_err_file = new_dir + "/somd.err" - process._rst_file = new_dir + "/somd.rst7" - process._top_file = new_dir + "/somd.prm7" - process._traj_file = new_dir + "/traj000000001.dcd" - process._restart_file = new_dir + "/latest.rst" - process._config_file = new_dir + "/somd.cfg" - process._pert_file = new_dir + "/somd.pert" - process._gradients_file = new_dir + "/gradients.dat" + process._std_out_file = _os.path.join(new_dir, "somd.out") + process._std_err_file = _os.path.join(new_dir, "somd.err") + process._rst_file = _os.path.join(new_dir, "somd.rst7") + process._top_file = _os.path.join(new_dir, "somd.prm7") + process._traj_file = _os.path.join(new_dir, "traj000000001.dcd") + process._restart_file = _os.path.join(new_dir, "latest.rst") + process._config_file = _os.path.join(new_dir, "somd.cfg") + process._pert_file = _os.path.join(new_dir, "somd.pert") + process._gradients_file = _os.path.join(new_dir, "gradients.dat") process._input_files = [ process._config_file, process._rst_file, @@ -2106,10 +2106,10 @@ def _initialise_runner(self, system): for line in new_config: f.write(line) - mdp = new_dir + "/gromacs.mdp" - gro = new_dir + "/gromacs.gro" - top = new_dir + "/gromacs.top" - tpr = new_dir + "/gromacs.tpr" + mdp = _os.path.join(new_dir, "gromacs.mdp") + gro = _os.path.join(new_dir, "gromacs.gro") + top = _os.path.join(new_dir, "gromacs.top") + tpr = _os.path.join(new_dir, "gromacs.tpr") # Use grompp to generate the portable binary run input file. _Process.Gromacs._generate_binary_run_file( @@ -2129,14 +2129,14 @@ def _initialise_runner(self, system): process._system = first_process._system.copy() process._protocol = self._protocol process._work_dir = new_dir - process._std_out_file = new_dir + "/gromacs.out" - process._std_err_file = new_dir + "/gromacs.err" - process._gro_file = new_dir + "/gromacs.gro" - process._top_file = new_dir + "/gromacs.top" - process._ref_file = new_dir + "/gromacs_ref.gro" - process._traj_file = new_dir + "/gromacs.trr" - process._config_file = new_dir + "/gromacs.mdp" - process._tpr_file = new_dir + "/gromacs.tpr" + process._std_out_file = _os.path.join(new_dir, "gromacs.out") + process._std_err_file = _os.path.join(new_dir, "gromacs.err") + process._gro_file = _os.path.join(new_dir, "gromacs.gro") + process._top_file = _os.path.join(new_dir, "gromacs.top") + process._ref_file = _os.path.join(new_dir, "gromacs_ref.gro") + process._traj_file = _os.path.join(new_dir, "gromacs.trr") + process._config_file = _os.path.join(new_dir, "gromacs.mdp") + process._tpr_file = _os.path.join(new_dir, "gromacs.tpr") process._input_files = [ process._config_file, process._gro_file, @@ -2165,14 +2165,14 @@ def _initialise_runner(self, system): process._system = first_process._system.copy() process._protocol = self._protocol process._work_dir = new_dir - process._std_out_file = new_dir + "/amber.out" - process._std_err_file = new_dir + "/amber.err" - process._rst_file = new_dir + "/amber.rst7" - process._top_file = new_dir + "/amber.prm7" - process._ref_file = new_dir + "/amber_ref.rst7" - process._traj_file = new_dir + "/amber.nc" - process._config_file = new_dir + "/amber.cfg" - process._nrg_file = new_dir + "/amber.nrg" + process._std_out_file = _os.path.join(new_dir, "amber.out") + process._std_err_file = _os.path.join(new_dir, "amber.err") + process._rst_file = _os.path.join(new_dir, "amber.rst7") + process._top_file = _os.path.join(new_dir, "amber.prm7") + process._ref_file = _os.path.join(new_dir, "amber_ref.rst7") + process._traj_file = _os.path.join(new_dir, "amber.nc") + process._config_file = _os.path.join(new_dir, "amber.cfg") + process._nrg_file = _os.path.join(new_dir, "amber.nrg") process._input_files = [ process._config_file, process._rst_file, diff --git a/python/BioSimSpace/Parameters/_Protocol/_amber.py b/python/BioSimSpace/Parameters/_Protocol/_amber.py index 69816c1e4..5857da3ff 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_amber.py +++ b/python/BioSimSpace/Parameters/_Protocol/_amber.py @@ -316,9 +316,6 @@ def run(self, molecule, work_dir=None, queue=None): else: is_smiles = False - # Create the file prefix. - prefix = work_dir + "/" - if not is_smiles: # Create a copy of the molecule. new_mol = molecule.copy() @@ -352,7 +349,10 @@ def run(self, molecule, work_dir=None, queue=None): ) # Prepend the working directory to the output file names. - output = [prefix + output[0], prefix + output[1]] + output = [ + _os.path.join(str(work_dir), output[0]), + _os.path.join(str(work_dir), output[1]), + ] try: # Load the parameterised molecule. (This could be a system of molecules.) @@ -443,9 +443,6 @@ def _run_tleap(self, molecule, work_dir): else: _molecule = molecule - # Create the file prefix. - prefix = work_dir + "/" - # Write the system to a PDB file. try: # LEaP expects residue numbering to be ascending and continuous. @@ -454,7 +451,7 @@ def _run_tleap(self, molecule, work_dir): )[0] renumbered_molecule = _Molecule(renumbered_molecule) _IO.saveMolecules( - prefix + "leap", + _os.path.join(str(work_dir), "leap"), renumbered_molecule, "pdb", property_map=self._property_map, @@ -500,7 +497,7 @@ def _run_tleap(self, molecule, work_dir): pruned_bond_records.append(bond) # Write the LEaP input file. - with open(prefix + "leap.txt", "w") as file: + with open(_os.path.join(str(work_dir), "leap.txt"), "w") as file: file.write("source %s\n" % ff) if self._water_model is not None: if self._water_model in ["tip4p", "tip5p"]: @@ -528,14 +525,14 @@ def _run_tleap(self, molecule, work_dir): # Generate the tLEaP command. command = "%s -f leap.txt" % _tleap_exe - with open(prefix + "README.txt", "w") as file: + with open(_os.path.join(str(work_dir), "README.txt"), "w") as file: # Write the command to file. file.write("# tLEaP was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. - stdout = open(prefix + "leap.out", "w") - stderr = open(prefix + "leap.err", "w") + stdout = open(_os.path.join(str(work_dir), "leap.out"), "w") + stderr = open(_os.path.join(str(work_dir), "leap.err"), "w") # Run tLEaP as a subprocess. proc = _subprocess.run( @@ -550,12 +547,12 @@ def _run_tleap(self, molecule, work_dir): # tLEaP doesn't return sensible error codes, so we need to check that # the expected output was generated. - if _os.path.isfile(prefix + "leap.top") and _os.path.isfile( - prefix + "leap.crd" - ): + if _os.path.isfile( + _os.path.join(str(work_dir), "leap.top") + ) and _os.path.isfile(_os.path.join(str(work_dir), "leap.crd")): # Check the output of tLEaP for missing atoms. if self._ensure_compatible: - if _has_missing_atoms(prefix + "leap.out"): + if _has_missing_atoms(_os.path.join(str(work_dir), "leap.top")): raise _ParameterisationError( "tLEaP added missing atoms. The topology is now " "inconsistent with the original molecule. Please " @@ -604,13 +601,13 @@ def _run_pdb2gmx(self, molecule, work_dir): else: _molecule = molecule - # Create the file prefix. - prefix = work_dir + "/" - # Write the system to a PDB file. try: _IO.saveMolecules( - prefix + "leap", _molecule, "pdb", property_map=self._property_map + _os.path.join(str(work_dir), "input"), + _molecule, + "pdb", + property_map=self._property_map, ) except Exception as e: msg = "Failed to write system to 'PDB' format." @@ -626,14 +623,14 @@ def _run_pdb2gmx(self, molecule, work_dir): % (_gmx_exe, supported_ff[self._forcefield]) ) - with open(prefix + "README.txt", "w") as file: + with open(_os.path.join(str(work_dir), "README.txt"), "w") as file: # Write the command to file. file.write("# pdb2gmx was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. - stdout = open(prefix + "pdb2gmx.out", "w") - stderr = open(prefix + "pdb2gmx.err", "w") + stdout = open(_os.path.join(str(work_dir), "pdb2gmx.out"), "w") + stderr = open(_os.path.join(str(work_dir), "pdb2gmx.err"), "w") # Run pdb2gmx as a subprocess. proc = _subprocess.run( @@ -647,9 +644,9 @@ def _run_pdb2gmx(self, molecule, work_dir): stderr.close() # Check for the expected output. - if _os.path.isfile(prefix + "output.gro") and _os.path.isfile( - prefix + "output.top" - ): + if _os.path.isfile( + _os.path.join(str(work_dir), "output.gro") + ) and _os.path.isfile(_os.path.join(str(work_dir), "output.top")): return ["output.gro", "output.top"] else: raise _ParameterisationError("pdb2gmx failed!") @@ -1010,9 +1007,6 @@ def run(self, molecule, work_dir=None, queue=None): if work_dir is None: work_dir = _os.getcwd() - # Create the file prefix. - prefix = work_dir + "/" - # Convert SMILES to a molecule. if isinstance(molecule, str): is_smiles = True @@ -1092,7 +1086,10 @@ def run(self, molecule, work_dir=None, queue=None): # Write the system to a PDB file. try: _IO.saveMolecules( - prefix + "antechamber", new_mol, "pdb", property_map=self._property_map + _os.path.join(str(work_dir), "antechamber"), + new_mol, + "pdb", + property_map=self._property_map, ) except Exception as e: msg = "Failed to write system to 'PDB' format." @@ -1108,14 +1105,14 @@ def run(self, molecule, work_dir=None, queue=None): + "-o antechamber.mol2 -fo mol2 -c %s -s 2 -nc %d" ) % (_antechamber_exe, self._version, self._charge_method.lower(), charge) - with open(prefix + "README.txt", "w") as file: + with open(_os.path.join(str(work_dir), "README.txt"), "w") as file: # Write the command to file. file.write("# Antechamber was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. - stdout = open(prefix + "antechamber.out", "w") - stderr = open(prefix + "antechamber.err", "w") + stdout = open(_os.path.join(str(work_dir), "antechamber.out"), "w") + stderr = open(_os.path.join(str(work_dir), "antechamber.err"), "w") # Run Antechamber as a subprocess. proc = _subprocess.run( @@ -1130,20 +1127,20 @@ def run(self, molecule, work_dir=None, queue=None): # Antechamber doesn't return sensible error codes, so we need to check that # the expected output was generated. - if _os.path.isfile(prefix + "antechamber.mol2"): + if _os.path.isfile(_os.path.join(str(work_dir), "antechamber.mol2")): # Run parmchk to check for missing parameters. command = ( "%s -s %d -i antechamber.mol2 -f mol2 " + "-o antechamber.frcmod" ) % (_parmchk_exe, self._version) - with open(prefix + "README.txt", "a") as file: + with open(_os.path.join(str(work_dir), "README.txt"), "a") as file: # Write the command to file. file.write("\n# ParmChk was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. - stdout = open(prefix + "parmchk.out", "w") - stderr = open(prefix + "parmchk.err", "w") + stdout = open(_os.path.join(str(work_dir), "parmchk.out"), "w") + stderr = open(_os.path.join(str(work_dir), "parmchk.err"), "w") # Run parmchk as a subprocess. proc = _subprocess.run( @@ -1157,7 +1154,7 @@ def run(self, molecule, work_dir=None, queue=None): stderr.close() # The frcmod file was created. - if _os.path.isfile(prefix + "antechamber.frcmod"): + if _os.path.isfile(_os.path.join(str(work_dir), "antechamber.frcmod")): # Now call tLEaP using the partially parameterised molecule and the frcmod file. # tLEap will run in the same working directory, using the Mol2 file generated by # Antechamber. @@ -1169,7 +1166,7 @@ def run(self, molecule, work_dir=None, queue=None): ff = _find_force_field("gaff2") # Write the LEaP input file. - with open(prefix + "leap.txt", "w") as file: + with open(_os.path.join(str(work_dir), "leap.txt"), "w") as file: file.write("source %s\n" % ff) file.write("mol = loadMol2 antechamber.mol2\n") file.write("loadAmberParams antechamber.frcmod\n") @@ -1179,14 +1176,14 @@ def run(self, molecule, work_dir=None, queue=None): # Generate the tLEaP command. command = "%s -f leap.txt" % _tleap_exe - with open(prefix + "README.txt", "a") as file: + with open(_os.path.join(str(work_dir), "README.txt"), "a") as file: # Write the command to file. file.write("\n# tLEaP was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. - stdout = open(prefix + "leap.out", "w") - stderr = open(prefix + "leap.err", "w") + stdout = open(_os.path.join(str(work_dir), "leap.out"), "w") + stderr = open(_os.path.join(str(work_dir), "leap.err"), "w") # Run tLEaP as a subprocess. proc = _subprocess.run( @@ -1201,12 +1198,12 @@ def run(self, molecule, work_dir=None, queue=None): # tLEaP doesn't return sensible error codes, so we need to check that # the expected output was generated. - if _os.path.isfile(prefix + "leap.top") and _os.path.isfile( - prefix + "leap.crd" - ): + if _os.path.isfile( + _os.path.join(str(work_dir), "leap.top") + ) and _os.path.isfile(_os.path.join(str(work_dir), "leap.crd")): # Check the output of tLEaP for missing atoms. if self._ensure_compatible: - if _has_missing_atoms(prefix + "leap.out"): + if _has_missing_atoms(_os.path.join(str(work_dir), "leap.out")): raise _ParameterisationError( "tLEaP added missing atoms. The topology is now " "inconsistent with the original molecule. Please " @@ -1217,7 +1214,10 @@ def run(self, molecule, work_dir=None, queue=None): # Load the parameterised molecule. (This could be a system of molecules.) try: par_mol = _IO.readMolecules( - [prefix + "leap.top", prefix + "leap.crd"] + [ + _os.path.join(str(work_dir), "leap.top"), + _os.path.join(str(work_dir), "leap.crd"), + ], ) # Extract single molecules. if par_mol.nMolecules() == 1: diff --git a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py index 018f43e4e..f6bc49605 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py @@ -214,9 +214,6 @@ def run(self, molecule, work_dir=None, queue=None): if work_dir is None: work_dir = _os.getcwd() - # Create the file prefix. - prefix = work_dir + "/" - # Flag whether the molecule is a SMILES string. if isinstance(molecule, str): is_smiles = True @@ -256,7 +253,7 @@ def run(self, molecule, work_dir=None, queue=None): # Write the molecule to SDF format. try: _IO.saveMolecules( - prefix + "molecule", + _os.path.join(str(work_dir), "molecule"), molecule, "sdf", property_map=self._property_map, @@ -275,7 +272,7 @@ def run(self, molecule, work_dir=None, queue=None): # Write the molecule to a PDB file. try: _IO.saveMolecules( - prefix + "molecule", + _os.path.join(str(work_dir), "molecule"), molecule, "pdb", property_map=self._property_map, @@ -291,7 +288,7 @@ def run(self, molecule, work_dir=None, queue=None): # Create an RDKit molecule from the PDB file. try: rdmol = _Chem.MolFromPDBFile( - prefix + "molecule.pdb", removeHs=False + _os.path.join(str(work_dir), "molecule.pdb"), removeHs=False ) except Exception as e: msg = "RDKit was unable to read the molecular PDB file!" @@ -303,7 +300,9 @@ def run(self, molecule, work_dir=None, queue=None): # Use RDKit to write back to SDF format. try: - writer = _Chem.SDWriter(prefix + "molecule.sdf") + writer = _Chem.SDWriter( + _os.path.join(str(work_dir), "molecule.sdf") + ) writer.write(rdmol) writer.close() except Exception as e: @@ -317,7 +316,9 @@ def run(self, molecule, work_dir=None, queue=None): # Create the Open Forcefield Molecule from the intermediate SDF file, # as recommended by @j-wags and @mattwthompson. try: - off_molecule = _OpenFFMolecule.from_file(prefix + "molecule.sdf") + off_molecule = _OpenFFMolecule.from_file( + _os.path.join(str(work_dir), "molecule.sdf") + ) except Exception as e: msg = "Unable to create OpenFF Molecule!" if _isVerbose(): @@ -383,8 +384,8 @@ def run(self, molecule, work_dir=None, queue=None): # Export AMBER format files. try: - interchange.to_prmtop(prefix + "interchange.prm7") - interchange.to_inpcrd(prefix + "interchange.rst7") + interchange.to_prmtop(_os.path.join(str(work_dir), "interchange.prmtop")) + interchange.to_inpcrd(_os.path.join(str(work_dir), "interchange.inpcrd")) except Exception as e: msg = "Unable to write Interchange object to AMBER format!" if _isVerbose(): @@ -396,7 +397,10 @@ def run(self, molecule, work_dir=None, queue=None): # Load the parameterised molecule. (This could be a system of molecules.) try: par_mol = _IO.readMolecules( - [prefix + "interchange.prm7", prefix + "interchange.rst7"] + [ + _os.path.join(str(work_dir), "interchange.prmtop"), + _os.path.join(str(work_dir), "interchange.inpcrd"), + ], ) # Extract single molecules. if par_mol.nMolecules() == 1: diff --git a/python/BioSimSpace/Process/_amber.py b/python/BioSimSpace/Process/_amber.py index 466216e87..8140729a4 100644 --- a/python/BioSimSpace/Process/_amber.py +++ b/python/BioSimSpace/Process/_amber.py @@ -235,15 +235,15 @@ def __init__( self._is_header = False # The names of the input files. - self._rst_file = "%s/%s.rst7" % (self._work_dir, name) - self._top_file = "%s/%s.prm7" % (self._work_dir, name) - self._ref_file = "%s/%s_ref.rst7" % (self._work_dir, name) + self._rst_file = _os.path.join(str(self._work_dir), f"{name}.rst7") + self._top_file = _os.path.join(str(self._work_dir), f"{name}.prm7") + self._ref_file = _os.path.join(str(self._work_dir), f"{name}_ref.rst7") # The name of the trajectory file. - self._traj_file = "%s/%s.nc" % (self._work_dir, name) + self._traj_file = _os.path.join(str(self._work_dir), f"{name}.nc") # Set the path for the AMBER configuration file. - self._config_file = "%s/%s.cfg" % (self._work_dir, name) + self._config_file = _os.path.join(str(self._work_dir), f"{name}.cfg") # Create the list of input files. self._input_files = [self._config_file, self._rst_file, self._top_file] @@ -400,7 +400,9 @@ def _generate_config(self): if auxiliary_files is not None: for file in auxiliary_files: file_name = _os.path.basename(file) - _shutil.copyfile(file, self._work_dir + f"/{file_name}") + _shutil.copyfile( + file, _os.path.join(str(self._work_dir), file_name) + ) self._input_files.append(self._plumed_config_file) # Expose the PLUMED specific member functions. @@ -534,7 +536,7 @@ def getSystem(self, block="AUTO"): _warnings.warn("The process exited with an error!") # Create the name of the restart CRD file. - restart = "%s/%s.crd" % (self._work_dir, self._name) + restart = _os.path.join(str(self._work_dir), "%s.crd" % self._name) # Check that the file exists. if _os.path.isfile(restart): diff --git a/python/BioSimSpace/Process/_gromacs.py b/python/BioSimSpace/Process/_gromacs.py index a35fc3531..7de88ac88 100644 --- a/python/BioSimSpace/Process/_gromacs.py +++ b/python/BioSimSpace/Process/_gromacs.py @@ -208,18 +208,18 @@ def __init__( self._energy_file = "%s/%s.edr" % (self._work_dir, name) # The names of the input files. - self._gro_file = "%s/%s.gro" % (self._work_dir, name) - self._top_file = "%s/%s.top" % (self._work_dir, name) - self._ref_file = "%s/%s_ref.gro" % (self._work_dir, name) + self._gro_file = _os.path.join(str(self._work_dir), f"{name}.gro") + self._top_file = _os.path.join(str(self._work_dir), f"{name}.top") + self._ref_file = _os.path.join(str(self._work_dir), f"{name}_ref.gro") # The name of the trajectory file. - self._traj_file = "%s/%s.trr" % (self._work_dir, name) + self._traj_file = _os.path.join(str(self._work_dir), f"{name}.trr") # The name of the output coordinate file. - self._crd_file = "%s/%s_out.gro" % (self._work_dir, name) + self._crd_file = _os.path.join(str(self._work_dir), f"{name}_out.gro") # Set the path for the GROMACS configuration file. - self._config_file = "%s/%s.mdp" % (self._work_dir, name) + self._config_file = _os.path.join(str(self._work_dir), f"{name}.mdp") # Create the list of input files. self._input_files = [self._config_file, self._gro_file, self._top_file] @@ -314,7 +314,7 @@ def _setup(self, **kwargs): ) # Create the binary input file name. - self._tpr_file = "%s/%s.tpr" % (self._work_dir, self._name) + self._tpr_file = _os.path.join(str(self._work_dir), f"{self._name}.tpr") self._input_files.append(self._tpr_file) # Generate the GROMACS configuration file. @@ -397,7 +397,9 @@ def _generate_config(self): if auxiliary_files is not None: for file in auxiliary_files: file_name = _os.path.basename(file) - _shutil.copyfile(file, self._work_dir + f"/{file_name}") + _shutil.copyfile( + file, _os.path.join(str(self._work_dir), file_name) + ) self._input_files.append(self._plumed_config_file) # Expose the PLUMED specific member functions. @@ -423,7 +425,9 @@ def _generate_config(self): if auxiliary_files is not None: for file in auxiliary_files: file_name = _os.path.basename(file) - _shutil.copyfile(file, self._work_dir + f"/{file_name}") + _shutil.copyfile( + file, _os.path.join(str(self._work_dir), file_name) + ) self._input_files.append(self._plumed_config_file) # Expose the PLUMED specific member functions. @@ -2122,7 +2126,9 @@ def _add_position_restraints(self): if len(restrained_atoms) > 0: # Create the file names. include_file = "posre_%04d.itp" % num_restraint - restraint_file = "%s/%s" % (self._work_dir, include_file) + restraint_file = _os.path.join( + str(self._work_dir), include_file + ) with open(restraint_file, "w") as file: # Write the header. @@ -2206,7 +2212,9 @@ def _add_position_restraints(self): if len(atom_idxs) > 0: # Create the file names. include_file = "posre_%04d.itp" % num_restraint - restraint_file = "%s/%s" % (self._work_dir, include_file) + restraint_file = _os.path.join( + str(self._work_dir), include_file + ) with open(restraint_file, "w") as file: # Write the header. @@ -2735,11 +2743,12 @@ def _getFrame(self, time): return old_system except: + raise _warnings.warn( "Failed to extract trajectory frame with trjconv. " "Try running 'getSystem' again." ) - frame = "%s/frame.gro" % self._work_dir + frame = _os.path.join(str(self._work_dir), "frame.gro") if _os.path.isfile(frame): _os.remove(frame) return None @@ -2759,7 +2768,7 @@ def _find_trajectory_file(self): # Check that the current trajectory file is found. if not _os.path.isfile(self._traj_file): # If not, first check for any trr extension. - traj_file = _glob.glob("%s/*.trr" % self._work_dir) + traj_file = _glob.glob(_os.path.join(str(self._work_dir), "*.trr")) # Store the number of trr files. num_trr = len(traj_file) @@ -2769,7 +2778,7 @@ def _find_trajectory_file(self): return traj_file[0] else: # Now check for any xtc files. - traj_file = _glob.glob("%s/*.xtc" % self._work_dir) + traj_file = _glob.glob(_os.path.join(str(self._work_dir), "*.xtc")) if len(traj_file) == 1: return traj_file[0] diff --git a/python/BioSimSpace/Process/_namd.py b/python/BioSimSpace/Process/_namd.py index 9811d26ab..787452905 100644 --- a/python/BioSimSpace/Process/_namd.py +++ b/python/BioSimSpace/Process/_namd.py @@ -148,17 +148,17 @@ def __init__( self._stdout_title = None # The names of the input files. - self._psf_file = "%s/%s.psf" % (self._work_dir, name) - self._top_file = "%s/%s.pdb" % (self._work_dir, name) - self._param_file = "%s/%s.params" % (self._work_dir, name) + self._psf_file = _os.path.join(str(self._work_dir), f"{name}.psf") + self._top_file = _os.path.join(str(self._work_dir), f"{name}.pdb") + self._param_file = _os.path.join(str(self._work_dir), f"{name}.params") self._velocity_file = None self._restraint_file = None # The name of the trajectory file. - self._traj_file = "%s/%s_out.dcd" % (self._work_dir, name) + self._traj_file = _os.path.join(str(self._work_dir), f"{name}_out.dcd") # Set the path for the NAMD configuration file. - self._config_file = "%s/%s.cfg" % (self._work_dir, name) + self._config_file = _os.path.join(str(self._work_dir), f"{name}.cfg") # Create the list of input files. self._input_files = [ @@ -443,9 +443,8 @@ def _generate_config(self): p = _SireIO.PDB2(restrained._sire_object, {prop: "restrained"}) # File name for the restraint file. - self._restraint_file = "%s/%s.restrained" % ( - self._work_dir, - self._name, + self._restraint_file = _os.path.join( + str(self._work_dir), f"{self._name}.restrained" ) # Write the PDB file. @@ -733,13 +732,19 @@ def getSystem(self, block="AUTO"): has_coor = False # First check for final configuration. - if _os.path.isfile("%s/%s_out.coor" % (self._work_dir, self._name)): - coor_file = "%s/%s_out.coor" % (self._work_dir, self._name) + if _os.path.isfile( + _os.path.join(str(self._work_dir), f"{self._name}_out.coor") + ): + coor_file = _os.path.join(str(self._work_dir), f"{self._name}_out.coor") has_coor = True # Otherwise check for a restart file. - elif _os.path.isfile("%s/%s_out.restart.coor" % (self._work_dir, self._name)): - coor_file = "%s/%s_out.restart.coor" % (self._work_dir, self._name) + elif _os.path.isfile( + _os.path.join(str(self._work_dir), f"{self._name}_out.restart.coor") + ): + coor_file = _os.path.join( + str(self._work_dir), f"{self._name}_out.restart.coor" + ) has_coor = True # Try to find an XSC file. @@ -747,13 +752,17 @@ def getSystem(self, block="AUTO"): has_xsc = False # First check for final XSC file. - if _os.path.isfile("%s/%s_out.xsc" % (self._work_dir, self._name)): - xsc_file = "%s/%s_out.xsc" % (self._work_dir, self._name) + if _os.path.isfile(_os.path.join(str(self._work_dir), f"{self._name}_out.xsc")): + xsc_file = _os.path.join(str(self._work_dir), f"{self._name}_out.xsc") has_xsc = True # Otherwise check for a restart XSC file. - elif _os.path.isfile("%s/%s_out.restart.xsc" % (self._work_dir, self._name)): - xsc_file = "%s/%s_out.restart.xsc" % (self._work_dir, self._name) + elif _os.path.isfile( + _os.path.join(str(self._work_dir), f"{self._name}_out.restart.xsc") + ): + xsc_file = _os.path.join( + str(self._work_dir), f"{self._name}_out.restart.xsc" + ) has_xsc = True # We found a coordinate file. diff --git a/python/BioSimSpace/Process/_openmm.py b/python/BioSimSpace/Process/_openmm.py index d11974d22..e19cc1f40 100644 --- a/python/BioSimSpace/Process/_openmm.py +++ b/python/BioSimSpace/Process/_openmm.py @@ -174,7 +174,7 @@ def __init__( self._stdout_dict = _process._MultiDict() # Store the name of the OpenMM log file. - self._log_file = "%s/%s.log" % (self._work_dir, name) + self._log_file = _os.path.join(str(self._work_dir), f"{name}.log") # Initialise the log file separator. self._record_separator = None @@ -184,16 +184,16 @@ def __init__( # The names of the input files. We choose to use AMBER files since they # are self-contained, but could equally work with GROMACS files. - self._rst_file = "%s/%s.rst7" % (self._work_dir, name) - self._top_file = "%s/%s.prm7" % (self._work_dir, name) - self._ref_file = "%s/%s_ref.rst7" % (self._work_dir, name) + self._rst_file = _os.path.join(str(self._work_dir), f"{name}.rst7") + self._top_file = _os.path.join(str(self._work_dir), f"{name}.prm7") + self._ref_file = _os.path.join(str(self._work_dir), f"{name}_ref.rst7") # The name of the trajectory file. - self._traj_file = "%s/%s.dcd" % (self._work_dir, name) + self._traj_file = _os.path.join(str(self._work_dir), f"{name}.dcd") # Set the path for the OpenMM Python script. (We use the concept of a # config file for consistency with other Process classes.) - self._config_file = "%s/%s_script.py" % (self._work_dir, name) + self._config_file = _os.path.join(str(self._work_dir), f"{name}_script.py") # Create the list of input files. self._input_files = [self._config_file, self._rst_file, self._top_file] @@ -772,7 +772,7 @@ def _generate_config(self): ) # Copy the file into the working directory. - _shutil.copyfile(path, self._work_dir + f"/{aux_file}") + _shutil.copyfile(path, _os.path.join(str(self._work_dir), aux_file)) # The following OpenMM native implementation of the funnel metadynamics protocol # is adapted from funnel_maker.py by Dominykas Lukauskis. @@ -970,9 +970,13 @@ def _generate_config(self): # Get the number of steps to date. step = 0 - if _os.path.isfile(f"{self._work_dir}/{self._name}.xml"): - if _os.path.isfile(f"{self._work_dir}/{self._name}.log"): - with open(f"{self._work_dir}/{self._name}.log", "r") as f: + if _os.path.isfile(_os.path.join(str(self._work_dir), f"{self._name}.xml")): + if _os.path.isfile( + _os.path.join(str(self._work_dir), f"{self._name}.log") + ): + with open( + _os.path.join(str(self._work_dir), f"{self._name}.log"), "r" + ) as f: lines = f.readlines() last_line = lines[-1].split() try: @@ -2031,8 +2035,10 @@ def _add_config_restart(self): self.addToConfig("else:") self.addToConfig(" is_restart = False") - if _os.path.isfile(f"{self._work_dir}/{self._name}.xml"): - with open(f"{self._work_dir}/{self._name}.log", "r") as f: + if _os.path.isfile(_os.path.join(str(self._work_dir), f"{self._name}.xml")): + with open( + _os.path.join(str(self._work_dir), f"{self._name}.log"), "r" + ) as f: lines = f.readlines() last_line = lines[-1].split() step = int(last_line[0]) diff --git a/python/BioSimSpace/Process/_plumed.py b/python/BioSimSpace/Process/_plumed.py index aad824087..7ee712696 100644 --- a/python/BioSimSpace/Process/_plumed.py +++ b/python/BioSimSpace/Process/_plumed.py @@ -117,8 +117,8 @@ def __init__(self, work_dir): self._work_dir = work_dir # Set the location of the HILLS and COLVAR files. - self._hills_file = "%s/HILLS" % self._work_dir - self._colvar_file = "%s/COLVAR" % self._work_dir + self._hills_file = _os.path.join(str(self._work_dir), "HILLS") + self._colvar_file = _os.path.join(str(self._work_dir), "COLVAR") # The number of collective variables and total number of components. self._num_colvar = 0 @@ -250,11 +250,11 @@ def _createMetadynamicsConfig(self, system, protocol, property_map={}): # Always remove pygtail offset files. try: - _os.remove("%s/COLVAR.offset" % self._work_dir) + _os.remove(_os.path.join(str(self._work_dir), "COLVAR.offset")) except: pass try: - _os.remove("%s/HILLS.offset" % self._work_dir) + _os.remove(_os.path.join(str(self._work_dir), "HILLS.offset")) except: pass @@ -627,7 +627,9 @@ def _createMetadynamicsConfig(self, system, protocol, property_map={}): colvar_string += " TYPE=%s" % colvar.getAlignmentType().upper() # Write the reference PDB file. - with open("%s/reference.pdb" % self._work_dir, "w") as file: + with open( + _os.path.join(str(self._work_dir), "reference.pdb"), "w" + ) as file: for line in colvar.getReferencePDB(): file.write(line + "\n") @@ -870,7 +872,9 @@ def _createMetadynamicsConfig(self, system, protocol, property_map={}): metad_string += ( " GRID_WFILE=GRID GRID_WSTRIDE=%s" % protocol.getHillFrequency() ) - if is_restart and _os.path.isfile(f"{self._work_dir}/GRID"): + if is_restart and _os.path.isfile( + _os.path.join(str(self._work_dir), "GRID") + ): metad_string += " GRID_RFILE=GRID" metad_string += " CALC_RCT" @@ -940,7 +944,7 @@ def _createSteeringConfig(self, system, protocol, property_map={}): # Always remove pygtail offset files. try: - _os.remove("%s/COLVAR.offset" % self._work_dir) + _os.remove(_os.path.join(str(self._work_dir), "COLVAR.offset")) except: pass @@ -1225,7 +1229,8 @@ def _createSteeringConfig(self, system, protocol, property_map={}): # Write the reference PDB file. with open( - "%s/reference_%i.pdb" % (self._work_dir, num_rmsd), "w" + _os.path.join(str(self._work_dir), "reference_%i.pdb" % num_rmsd), + "w", ) as file: for line in colvar.getReferencePDB(): file.write(line + "\n") @@ -1449,8 +1454,8 @@ def getFreeEnergy(self, index=None, stride=None, kt=_Types.Energy(1.0, "kt")): raise ValueError("'kt' must have value > 0") # Delete any existing FES directotry and create a new one. - _shutil.rmtree(f"{self._work_dir}/fes", ignore_errors=True) - _os.makedirs(f"{self._work_dir}/fes") + _shutil.rmtree(_os.path.join(str(self._work_dir), "fes"), ignore_errors=True) + _os.makedirs(_os.path.join(str(self._work_dir), "fes")) # Create the command string. command = "%s sum_hills --hills ../HILLS --mintozero" % self._exe @@ -1466,7 +1471,7 @@ def getFreeEnergy(self, index=None, stride=None, kt=_Types.Energy(1.0, "kt")): free_energies = [] # Move to the working directory. - with _Utils.cd(self._work_dir + "/fes"): + with _Utils.cd(_os.path.join(str(self._work_dir), "fes")): # Run the sum_hills command as a background process. proc = _subprocess.run( _Utils.command_split(command), @@ -1556,7 +1561,7 @@ def getFreeEnergy(self, index=None, stride=None, kt=_Types.Energy(1.0, "kt")): _os.remove(fes) # Remove the FES output directory. - _shutil.rmtree(f"{self._work_dir}/fes", ignore_errors=True) + _shutil.rmtree(_os.path.join(str(self._work_dir), "fes"), ignore_errors=True) return tuple(free_energies) diff --git a/python/BioSimSpace/Process/_process.py b/python/BioSimSpace/Process/_process.py index a4ef0ebf2..3457e492c 100644 --- a/python/BioSimSpace/Process/_process.py +++ b/python/BioSimSpace/Process/_process.py @@ -281,11 +281,11 @@ def __init__( self._work_dir = _Utils.WorkDir(work_dir) # Files for redirection of stdout and stderr. - self._stdout_file = "%s/%s.out" % (self._work_dir, name) - self._stderr_file = "%s/%s.err" % (self._work_dir, name) + self._stdout_file = _os.path.join(str(self._work_dir), f"{name}.out") + self._stderr_file = _os.path.join(str(self._work_dir), f"{name}.err") # Files for metadynamics simulation with PLUMED. - self._plumed_config_file = "%s/plumed.dat" % self._work_dir + self._plumed_config_file = _os.path.join(str(self._work_dir), "plumed.dat") self._plumed_config = None # Initialise the configuration file string list. @@ -349,13 +349,13 @@ def _clear_output(self): self._stderr = [] # Clean up any existing offset files. - offset_files = _glob.glob("%s/*.offset" % self._work_dir) + offset_files = _glob.glob(_os.path.join(str(self._work_dir), "*.offset")) # Remove any HILLS or COLVAR files from the list. These will be dealt # with by the PLUMED interface. try: - offset_files.remove("%s/COLVAR.offset" % self._work_dir) - offset_files.remove("%s/HILLS.offset" % self._work_dir) + offset_files.remove(_os.path.join(str(self._work_dir), "COLVAR.offset")) + offset_files.remove(_os.path.join(str(self._work_dir), "HILLS.offset")) except: pass @@ -1240,7 +1240,7 @@ def getOutput(self, name=None, block="AUTO", file_link=False): zipname = "%s.zip" % name # Glob all of the output files. - output = _glob.glob("%s/*" % self._work_dir) + output = _glob.glob(_os.path.join(str(self._work_dir), "*")) with _zipfile.ZipFile(zipname, "w") as zip: # Loop over all of the file outputs. diff --git a/python/BioSimSpace/Process/_process_runner.py b/python/BioSimSpace/Process/_process_runner.py index 5c45c4f04..fa1f08cee 100644 --- a/python/BioSimSpace/Process/_process_runner.py +++ b/python/BioSimSpace/Process/_process_runner.py @@ -843,7 +843,9 @@ def _nest_directories(self, processes): # Loop over each process. for process in processes: # Create the new working directory name. - new_dir = "%s/%s" % (self._work_dir, _os.path.basename(process._work_dir)) + new_dir = _os.path.join( + self._work_dir, _os.path.basename(process._work_dir) + ) # Create a new process object using the nested directory. if process._package_name == "SOMD": diff --git a/python/BioSimSpace/Process/_somd.py b/python/BioSimSpace/Process/_somd.py index 1f3094daf..4b66b8646 100644 --- a/python/BioSimSpace/Process/_somd.py +++ b/python/BioSimSpace/Process/_somd.py @@ -227,23 +227,23 @@ def __init__( raise IOError("SOMD executable doesn't exist: '%s'" % exe) # The names of the input files. - self._rst_file = "%s/%s.rst7" % (self._work_dir, name) - self._top_file = "%s/%s.prm7" % (self._work_dir, name) + self._rst_file = _os.path.join(str(self._work_dir), f"{name}.rst7") + self._top_file = _os.path.join(str(self._work_dir), f"{name}.prm7") # The name of the trajectory file. - self._traj_file = "%s/traj000000001.dcd" % self._work_dir + self._traj_file = _os.path.join(str(self._work_dir), "traj000000001.dcd") # The name of the restart file. - self._restart_file = "%s/latest.pdb" % self._work_dir + self._restart_file = _os.path.join(str(self._work_dir), "latest.pdb") # Set the path for the SOMD configuration file. - self._config_file = "%s/%s.cfg" % (self._work_dir, name) + self._config_file = _os.path.join(str(self._work_dir), f"{name}.cfg") # Set the path for the perturbation file. - self._pert_file = "%s/%s.pert" % (self._work_dir, name) + self._pert_file = _os.path.join(str(self._work_dir), f"{name}.pert") # Set the path for the gradient file and create the gradient list. - self._gradient_file = "%s/gradients.dat" % self._work_dir + self._gradient_file = _os.path.join(str(self._work_dir), "gradients.dat") self._gradients = [] # Create the list of input files. @@ -871,26 +871,26 @@ def _clear_output(self): # Delete any restart and trajectory files in the working directory. - file = "%s/sim_restart.s3" % self._work_dir + file = _os.path.join(str(self._work_dir), "sim_restart.s3") if _os.path.isfile(file): _os.remove(file) - file = "%s/SYSTEM.s3" % self._work_dir + file = _os.path.join(str(self._work_dir), "SYSTEM.s3") if _os.path.isfile(file): _os.remove(file) - files = _glob.glob("%s/traj*.dcd" % self._work_dir) + files = _glob.glob(_os.path.join(str(self._work_dir), "traj*.dcd")) for file in files: if _os.path.isfile(file): _os.remove(file) # Additional files for free energy simulations. if isinstance(self._protocol, _Protocol.FreeEnergy): - file = "%s/gradients.dat" % self._work_dir + file = _os.path.join(str(self._work_dir), "gradients.dat") if _os.path.isfile(file): _os.remove(file) - file = "%s/simfile.dat" % self._work_dir + file = _os.path.join(str(self._work_dir), "simfile.dat") if _os.path.isfile(file): _os.remove(file) diff --git a/python/BioSimSpace/Trajectory/_trajectory.py b/python/BioSimSpace/Trajectory/_trajectory.py index 7b5f9d45e..1b7ac59ed 100644 --- a/python/BioSimSpace/Trajectory/_trajectory.py +++ b/python/BioSimSpace/Trajectory/_trajectory.py @@ -157,7 +157,7 @@ def getFrame(trajectory, topology, index, system=None, property_map={}): errors = [] is_sire = False is_mdanalysis = False - pdb_file = work_dir + f"/{str(_uuid.uuid4())}.pdb" + pdb_file = _os.path.join(str(work_dir), f"{str(_uuid.uuid4())}.pdb") try: frame = _sire_load( [trajectory, topology], @@ -169,7 +169,7 @@ def getFrame(trajectory, topology, index, system=None, property_map={}): except Exception as e: errors.append(f"Sire: {str(e)}") try: - frame_file = work_dir + f"/{str(_uuid.uuid4())}.rst7" + frame_file = _os.path.join(str(work_dir), f"{str(_uuid.uuid4())}.rst7") frame = _mdtraj.load_frame(trajectory, index, top=topology) frame.save(frame_file, force_overwrite=True) frame.save(pdb_file, force_overwrite=True) @@ -178,7 +178,7 @@ def getFrame(trajectory, topology, index, system=None, property_map={}): errors.append(f"MDTraj: {str(e)}") # Try to load the frame with MDAnalysis. try: - frame_file = work_dir + f"/{str(_uuid.uuid4())}.gro" + frame_file = _os.path.join(str(work_dir), f"{str(_uuid.uuid4())}.gro") universe = _mdanalysis.Universe(topology, trajectory) universe.trajectory.trajectory[index] with _warnings.catch_warnings(): @@ -615,7 +615,9 @@ def getTrajectory(self, format="auto"): # If this is a PRM7 file, copy to PARM7. if extension == ".prm7": # Set the path to the temporary topology file. - top_file = self._work_dir + f"/{str(_uuid.uuid4())}.parm7" + top_file = _os.path.join( + str(self._work_dir), f"{str(_uuid.uuid4())}.parm7" + ) # Copy the topology to a file with the correct extension. _shutil.copyfile(self._top_file, top_file) @@ -761,16 +763,20 @@ def getFrames(self, indices=None): # Write the current frame to file. - pdb_file = self._work_dir + f"/{str(_uuid.uuid4())}.pdb" + pdb_file = _os.path.join(str(self._work_dir), f"{str(_uuid.uuid4())}.pdb") if self._backend == "SIRE": frame = self._trajectory[x] elif self._backend == "MDTRAJ": - frame_file = self._work_dir + f"/{str(_uuid.uuid4())}.rst7" + frame_file = _os.path.join( + str(self._work_dir), f"{str(_uuid.uuid4())}.rst7" + ) self._trajectory[x].save(frame_file, force_overwrite=True) self._trajectory[x].save(pdb_file, force_overwrite=True) elif self._backend == "MDANALYSIS": - frame_file = self._work_dir + f"/{str(_uuid.uuid4())}.gro" + frame_file = _os.path.join( + str(self._work_dir), f"{str(_uuid.uuid4())}.gro" + ) self._trajectory.trajectory[x] with _warnings.catch_warnings(): _warnings.simplefilter("ignore") @@ -1110,8 +1116,8 @@ def _split_molecules(frame, pdb, reference, work_dir, property_map={}): formats = reference.fileFormat() # Write the frame coordinates/velocities to file. - coord_file = work_dir + f"/{str(_uuid.uuid4())}.coords" - top_file = work_dir + f"/{str(_uuid.uuid4())}.top" + coord_file = _os.path.join(str(work_dir), f"{str(_uuid.uuid4())}.coords") + top_file = _os.path.join(str(work_dir), f"{str(_uuid.uuid4())}.top") frame.writeToFile(coord_file) # Whether we've parsed as a PDB file. From bd60625d01322ff1ef127d5f06885ef9bb61ce5a Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 10:22:38 +0100 Subject: [PATCH 099/121] Preserve fileformat property when creating a system from a molecule. [closes #273] --- python/BioSimSpace/_SireWrappers/_system.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/BioSimSpace/_SireWrappers/_system.py b/python/BioSimSpace/_SireWrappers/_system.py index 2d458890b..dbb8db8fc 100644 --- a/python/BioSimSpace/_SireWrappers/_system.py +++ b/python/BioSimSpace/_SireWrappers/_system.py @@ -88,12 +88,20 @@ def __init__(self, system): sire_object = _SireSystem.System("BioSimSpace_System.") super().__init__(sire_object) self.addMolecules(_Molecule(system)) + if "fileformat" in system.propertyKeys(): + self._sire_object.setProperty( + "fileformat", system.property("fileformat") + ) # A BioSimSpace Molecule object. elif isinstance(system, _Molecule): sire_object = _SireSystem.System("BioSimSpace_System.") super().__init__(sire_object) self.addMolecules(system) + if "fileformat" in system._sire_object.propertyKeys(): + self._sire_object.setProperty( + "fileformat", system._sire_object.property("fileformat") + ) # A BioSimSpace Molecules object. elif isinstance(system, _Molecules): From a42aa2ea2accbf0e3c70ca7c5bc168087b3faf44 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 10:23:26 +0100 Subject: [PATCH 100/121] Make sure base protocol returns None for getRestraint. [closes #274] --- .../Protocol/_position_restraint_mixin.py | 5 +++-- python/BioSimSpace/Protocol/_protocol.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/python/BioSimSpace/Protocol/_position_restraint_mixin.py b/python/BioSimSpace/Protocol/_position_restraint_mixin.py index 8374bf8ad..3211558d1 100644 --- a/python/BioSimSpace/Protocol/_position_restraint_mixin.py +++ b/python/BioSimSpace/Protocol/_position_restraint_mixin.py @@ -110,13 +110,14 @@ def __eq__(self, other): ) def getRestraint(self): - """Return the type of restraint.. + """ + Return the type of restraint. Returns ------- restraint : str, [int] - The type of restraint. + The type of restraint, either a keyword or a list of atom indices. """ return self._restraint diff --git a/python/BioSimSpace/Protocol/_protocol.py b/python/BioSimSpace/Protocol/_protocol.py index 9d37e004f..ad52a55e1 100644 --- a/python/BioSimSpace/Protocol/_protocol.py +++ b/python/BioSimSpace/Protocol/_protocol.py @@ -40,6 +40,23 @@ def __init__(self): # Flag that the protocol hasn't been customised. self._is_customised = False + def getRestraint(self): + """ + Return the type of restraint. + + Returns + ------- + + restraint : str, [int] + The type of restraint, either a keyword or a list of atom indices. + """ + from ._position_restraint_mixin import _PositionRestraintMixin + + if isinstance(self, _PositionRestraintMixin): + return self._restraint + else: + return None + def _setCustomised(self, is_customised): """ Internal function to flag whether a protocol has been customised. From 65115b28983754c5afb792a176c794898326bc0b Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 10:23:56 +0100 Subject: [PATCH 101/121] Add missing thermostat_time_constant to metadynamics protocol. [closes #275] --- python/BioSimSpace/Protocol/_metadynamics.py | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/python/BioSimSpace/Protocol/_metadynamics.py b/python/BioSimSpace/Protocol/_metadynamics.py index 99b49320e..8e6b25e82 100644 --- a/python/BioSimSpace/Protocol/_metadynamics.py +++ b/python/BioSimSpace/Protocol/_metadynamics.py @@ -48,6 +48,7 @@ def __init__( runtime=_Types.Time(1, "nanosecond"), temperature=_Types.Temperature(300, "kelvin"), pressure=_Types.Pressure(1, "atmosphere"), + thermostat_time_constant=_Types.Time(1, "picosecond"), hill_height=_Types.Energy(1, "kj per mol"), hill_frequency=1000, report_interval=1000, @@ -76,6 +77,9 @@ def __init__( pressure : :class:`Pressure ` The pressure. Pass pressure=None to use the NVT ensemble. + thermostat_time_constant : :class:`Time ` + Time constant for thermostat coupling. + hill_height : :class:`Energy ` The height of the Gaussian hills. @@ -123,6 +127,9 @@ def __init__( else: self._pressure = None + # Set the thermostat time constant. + self.setThermostatTimeConstant(thermostat_time_constant) + # Set the hill parameters: height, frequency. self.setHillHeight(hill_height) self.setHillFrequency(hill_frequency) @@ -384,6 +391,42 @@ def setPressure(self, pressure): "'pressure' must be of type 'str' or 'BioSimSpace.Types.Pressure'" ) + def getThermostatTimeConstant(self): + """ + Return the time constant for the thermostat. + + Returns + ------- + + runtime : :class:`Time ` + The time constant for the thermostat. + """ + return self._thermostat_time_constant + + def setThermostatTimeConstant(self, thermostat_time_constant): + """ + Set the time constant for the thermostat. + + Parameters + ---------- + + thermostat_time_constant : str, :class:`Time ` + The time constant for the thermostat. + """ + if isinstance(thermostat_time_constant, str): + try: + self._thermostat_time_constant = _Types.Time(thermostat_time_constant) + except: + raise ValueError( + "Unable to parse 'thermostat_time_constant' string." + ) from None + elif isinstance(thermostat_time_constant, _Types.Time): + self._thermostat_time_constant = thermostat_time_constant + else: + raise TypeError( + "'thermostat_time_constant' must be of type 'str' or 'BioSimSpace.Types.Time'" + ) + def getHillHeight(self): """ Return the height of the Gaussian hills. From 557bbacb29b5fffdbcf44e763727d5399c5d3e57 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 10:28:56 +0100 Subject: [PATCH 102/121] Add temperature control to Metadynamics and Steering protocols. --- python/BioSimSpace/_Config/_amber.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/python/BioSimSpace/_Config/_amber.py b/python/BioSimSpace/_Config/_amber.py index 861fa5d72..34d6ad84d 100644 --- a/python/BioSimSpace/_Config/_amber.py +++ b/python/BioSimSpace/_Config/_amber.py @@ -321,10 +321,7 @@ def createConfig( ) # Temperature control. - if not isinstance( - self._protocol, - (_Protocol.Metadynamics, _Protocol.Steering, _Protocol.Minimisation), - ): + if not isinstance(self._protocol, _Protocol.Minimisation): # Langevin dynamics. protocol_dict["ntt"] = 3 # Collision frequency (1 / ps). From fed23af4894f704688c24985d9ceb3d154fd5221 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 10:44:32 +0100 Subject: [PATCH 103/121] Refactor fixtures. --- tests/Convert/test_convert.py | 5 ---- tests/FreeEnergy/test_relative.py | 11 -------- tests/Process/test_amber.py | 28 -------------------- tests/Process/test_gromacs.py | 17 ------------- tests/Process/test_namd.py | 12 --------- tests/Process/test_openmm.py | 6 ----- tests/Process/test_single_point_energy.py | 22 +++++++++------- tests/Process/test_somd.py | 17 ------------- tests/Protocol/test_protocol.py | 6 ----- tests/Solvent/test_solvent.py | 8 +++--- tests/Stream/test_stream.py | 5 ---- tests/Trajectory/test_trajectory.py | 6 ----- tests/_SireWrappers/test_molecule.py | 5 ---- tests/_SireWrappers/test_search_result.py | 6 ----- tests/_SireWrappers/test_system.py | 6 ----- tests/conftest.py | 31 +++++++++++++++++++++++ 16 files changed, 48 insertions(+), 143 deletions(-) diff --git a/tests/Convert/test_convert.py b/tests/Convert/test_convert.py index f2f4efbb8..8823e4ef9 100644 --- a/tests/Convert/test_convert.py +++ b/tests/Convert/test_convert.py @@ -5,11 +5,6 @@ import pytest -@pytest.fixture(scope="session") -def system(): - return BSS.IO.readMolecules(["tests/input/ala.crd", "tests/input/ala.top"]) - - def test_system(system): """ Check that system conversions work as expected. diff --git a/tests/FreeEnergy/test_relative.py b/tests/FreeEnergy/test_relative.py index 6d01fe81f..6808b0ed7 100644 --- a/tests/FreeEnergy/test_relative.py +++ b/tests/FreeEnergy/test_relative.py @@ -9,17 +9,6 @@ from tests.conftest import url, has_alchemlyb, has_gromacs -@pytest.fixture(scope="module") -def perturbable_system(): - """Re-use the same perturbable system for each test.""" - return BSS.IO.readPerturbableSystem( - f"{url}/perturbable_system0.prm7", - f"{url}/perturbable_system0.rst7", - f"{url}/perturbable_system1.prm7", - f"{url}/perturbable_system1.rst7", - ) - - @pytest.fixture(scope="module") def fep_output(): """Path to a temporary directory containing FEP output.""" diff --git a/tests/Process/test_amber.py b/tests/Process/test_amber.py index a6626047e..735f2af46 100644 --- a/tests/Process/test_amber.py +++ b/tests/Process/test_amber.py @@ -13,12 +13,6 @@ restraints = BSS.Protocol._position_restraint_mixin._PositionRestraintMixin.restraints() -@pytest.fixture(scope="session") -def system(): - """Re-use the same molecuar system for each test.""" - return BSS.IO.readMolecules(["tests/input/ala.top", "tests/input/ala.crd"]) - - @pytest.fixture(scope="session") def rna_system(): """An RNA system for re-use.""" @@ -35,28 +29,6 @@ def large_protein_system(): ) -@pytest.fixture(scope="module") -def perturbable_system(): - """Re-use the same perturbable system for each test.""" - return BSS.IO.readPerturbableSystem( - f"{url}/perturbable_system0.prm7", - f"{url}/perturbable_system0.rst7", - f"{url}/perturbable_system1.prm7", - f"{url}/perturbable_system1.rst7", - ) - - -@pytest.fixture(scope="module") -def solvated_perturbable_system(): - """Re-use the same solvated perturbable system for each test.""" - return BSS.IO.readPerturbableSystem( - f"{url}/solvated_perturbable_system0.prm7", - f"{url}/solvated_perturbable_system0.rst7", - f"{url}/solvated_perturbable_system1.prm7", - f"{url}/solvated_perturbable_system1.rst7", - ) - - @pytest.mark.skipif(has_amber is False, reason="Requires AMBER to be installed.") @pytest.mark.parametrize("restraint", restraints) def test_minimise(system, restraint): diff --git a/tests/Process/test_gromacs.py b/tests/Process/test_gromacs.py index a321f033d..5de10a95a 100644 --- a/tests/Process/test_gromacs.py +++ b/tests/Process/test_gromacs.py @@ -19,23 +19,6 @@ restraints = BSS.Protocol._position_restraint_mixin._PositionRestraintMixin.restraints() -@pytest.fixture(scope="session") -def system(): - """Re-use the same molecuar system for each test.""" - return BSS.IO.readMolecules(["tests/input/ala.top", "tests/input/ala.crd"]) - - -@pytest.fixture(scope="session") -def perturbable_system(): - """Re-use the same perturbable system for each test.""" - return BSS.IO.readPerturbableSystem( - f"{url}/complex_vac0.prm7.bz2", - f"{url}/complex_vac0.rst7.bz2", - f"{url}/complex_vac1.prm7.bz2", - f"{url}/complex_vac1.rst7.bz2", - ) - - @pytest.mark.skipif(has_gromacs is False, reason="Requires GROMACS to be installed.") @pytest.mark.parametrize("restraint", restraints) def test_minimise(system, restraint): diff --git a/tests/Process/test_namd.py b/tests/Process/test_namd.py index 03c550d46..990ff1473 100644 --- a/tests/Process/test_namd.py +++ b/tests/Process/test_namd.py @@ -8,18 +8,6 @@ restraints = BSS.Protocol._position_restraint_mixin._PositionRestraintMixin.restraints() -@pytest.fixture(scope="session") -def system(): - """Re-use the same molecuar system for each test.""" - return BSS.IO.readMolecules( - [ - "tests/input/alanin.psf", - f"tests/input/alanin.pdb", - f"tests/input/alanin.params", - ] - ) - - @pytest.mark.skipif(has_namd is False, reason="Requires NAMD to be installed.") @pytest.mark.parametrize("restraint", restraints) def test_minimise(system, restraint): diff --git a/tests/Process/test_openmm.py b/tests/Process/test_openmm.py index 7925bd91b..a7968173a 100644 --- a/tests/Process/test_openmm.py +++ b/tests/Process/test_openmm.py @@ -8,12 +8,6 @@ restraints = BSS.Protocol._position_restraint_mixin._PositionRestraintMixin.restraints() -@pytest.fixture(scope="session") -def system(): - """Re-use the same molecuar system for each test.""" - return BSS.IO.readMolecules(["tests/input/ala.top", "tests/input/ala.crd"]) - - @pytest.mark.parametrize("restraint", restraints) def test_minimise(system, restraint): """Test a minimisation protocol.""" diff --git a/tests/Process/test_single_point_energy.py b/tests/Process/test_single_point_energy.py index e252cfb99..8fc3a604e 100644 --- a/tests/Process/test_single_point_energy.py +++ b/tests/Process/test_single_point_energy.py @@ -5,8 +5,8 @@ from tests.conftest import url, has_amber, has_gromacs -@pytest.fixture(scope="session") -def system(): +@pytest.fixture(scope="module") +def ubiquitin_system(): """Re-use the same molecuar system for each test.""" return BSS.IO.readMolecules( [f"{url}/ubiquitin.prm7.bz2", f"{url}/ubiquitin.rst7.bz2"] @@ -17,17 +17,19 @@ def system(): has_amber is False or has_gromacs is False, reason="Requires that both AMBER and GROMACS are installed.", ) -def test_amber_gromacs(system): +def test_amber_gromacs(ubiquitin_system): """Single point energy comparison between AMBER and GROMACS.""" # Create a single-step minimisation protocol. protocol = BSS.Protocol.Minimisation(steps=1) # Create a process to run with AMBER. - process_amb = BSS.Process.Amber(system, protocol) + process_amb = BSS.Process.Amber(ubiquitin_system, protocol) # Create a process to run with GROMACS. - process_gmx = BSS.Process.Gromacs(system, protocol, extra_options={"nsteps": 0}) + process_gmx = BSS.Process.Gromacs( + ubiquitin_system, protocol, extra_options={"nsteps": 0} + ) # Run the AMBER process and wait for it to finish. process_amb.start() @@ -57,23 +59,25 @@ def test_amber_gromacs(system): has_amber is False or has_gromacs is False, reason="Requires that both AMBER and GROMACS are installed.", ) -def test_amber_gromacs_triclinic(system): +def test_amber_gromacs_triclinic(ubiquitin_system): """Single point energy comparison between AMBER and GROMACS in a triclinic box.""" # Swap the space for a triclinic cell (truncated octahedron). from sire.legacy.Vol import TriclinicBox triclinic_box = TriclinicBox.truncatedOctahedron(50) - system._sire_object.setProperty("space", triclinic_box) + ubiquitin_system._sire_object.setProperty("space", triclinic_box) # Create a single-step minimisation protocol. protocol = BSS.Protocol.Minimisation(steps=1) # Create a process to run with AMBER. - process_amb = BSS.Process.Amber(system, protocol) + process_amb = BSS.Process.Amber(ubiquitin_system, protocol) # Create a process to run with GROMACS. - process_gmx = BSS.Process.Gromacs(system, protocol, extra_options={"nsteps": 0}) + process_gmx = BSS.Process.Gromacs( + ubiquitin_system, protocol, extra_options={"nsteps": 0} + ) # Run the AMBER process and wait for it to finish. process_amb.start() diff --git a/tests/Process/test_somd.py b/tests/Process/test_somd.py index 2220e589e..f7eedc52c 100644 --- a/tests/Process/test_somd.py +++ b/tests/Process/test_somd.py @@ -9,23 +9,6 @@ url = BSS.tutorialUrl() -@pytest.fixture(scope="session") -def system(): - """Re-use the same molecuar system for each test.""" - return BSS.IO.readMolecules(["tests/input/ala.top", "tests/input/ala.crd"]) - - -@pytest.fixture(scope="session") -def perturbable_system(): - """Re-use the same perturbable system for each test.""" - return BSS.IO.readPerturbableSystem( - f"{url}/perturbable_system0.prm7", - f"{url}/perturbable_system0.rst7", - f"{url}/perturbable_system1.prm7", - f"{url}/perturbable_system1.rst7", - ) - - def test_minimise(system): """Test a minimisation protocol.""" diff --git a/tests/Protocol/test_protocol.py b/tests/Protocol/test_protocol.py index c38e8e553..b0614b6fa 100644 --- a/tests/Protocol/test_protocol.py +++ b/tests/Protocol/test_protocol.py @@ -6,12 +6,6 @@ # using strings of unit-based types. -@pytest.fixture(scope="session") -def system(): - """Re-use the same molecuar system for each test.""" - return BSS.IO.readMolecules(["tests/input/ala.top", "tests/input/ala.crd"]) - - def test_equilibration(): # Instantiate from types. p0 = BSS.Protocol.Equilibration( diff --git a/tests/Solvent/test_solvent.py b/tests/Solvent/test_solvent.py index 185bb6373..02afe1f06 100644 --- a/tests/Solvent/test_solvent.py +++ b/tests/Solvent/test_solvent.py @@ -11,7 +11,7 @@ @pytest.fixture(scope="module") -def system(): +def kigaki_system(): return BSS.IO.readMolecules( BSS.IO.expand( BSS.tutorialUrl(), ["kigaki_xtal_water.gro", "kigaki_xtal_water.top"] @@ -25,7 +25,7 @@ def system(): [partial(BSS.Solvent.solvate, "tip3p"), BSS.Solvent.tip3p], ) @pytest.mark.skipif(not has_gromacs, reason="Requires GROMACS to be installed") -def test_crystal_water(system, match_water, function): +def test_crystal_water(kigaki_system, match_water, function): """ Test that user defined crystal waters can be preserved during solvation and on write to GroTop format. @@ -35,13 +35,13 @@ def test_crystal_water(system, match_water, function): if match_water: num_matches = 0 else: - num_matches = len(system.search("resname COF").molecules()) + num_matches = len(kigaki_system.search("resname COF").molecules()) # Create the box parameters. box, angles = BSS.Box.cubic(5.5 * BSS.Units.Length.nanometer) # Create the solvated system. - solvated = function(system, box, angles, match_water=match_water) + solvated = function(kigaki_system, box, angles, match_water=match_water) # Search for the crystal waters in the solvated system. try: diff --git a/tests/Stream/test_stream.py b/tests/Stream/test_stream.py index 12f169dec..73788bd7e 100644 --- a/tests/Stream/test_stream.py +++ b/tests/Stream/test_stream.py @@ -4,11 +4,6 @@ import BioSimSpace as BSS -@pytest.fixture -def system(scope="session"): - return BSS.IO.readMolecules(["tests/input/ala.top", "tests/input/ala.crd"]) - - @pytest.fixture(autouse=True) def run_around_tests(): yield diff --git a/tests/Trajectory/test_trajectory.py b/tests/Trajectory/test_trajectory.py index b03a554ed..08457529d 100644 --- a/tests/Trajectory/test_trajectory.py +++ b/tests/Trajectory/test_trajectory.py @@ -13,12 +13,6 @@ def wrap(arg): from tests.conftest import url, has_mdanalysis, has_mdtraj -@pytest.fixture(scope="session") -def system(): - """A system object with the same topology as the trajectories.""" - return BSS.IO.readMolecules(["tests/input/ala.top", "tests/input/ala.crd"]) - - @pytest.fixture(scope="session") def traj_sire(system): """A trajectory object using the Sire backend.""" diff --git a/tests/_SireWrappers/test_molecule.py b/tests/_SireWrappers/test_molecule.py index 28b6c28df..8152c038a 100644 --- a/tests/_SireWrappers/test_molecule.py +++ b/tests/_SireWrappers/test_molecule.py @@ -5,11 +5,6 @@ from tests.conftest import url, has_amber -@pytest.fixture(scope="session") -def system(): - return BSS.IO.readMolecules(["tests/input/ala.top", "tests/input/ala.crd"]) - - @pytest.mark.skipif(has_amber is False, reason="Requires AMBER to be installed.") def test_makeCompatibleWith(): # Load the original PDB file. In this representation the system contains diff --git a/tests/_SireWrappers/test_search_result.py b/tests/_SireWrappers/test_search_result.py index b38cb40c2..55aeda098 100644 --- a/tests/_SireWrappers/test_search_result.py +++ b/tests/_SireWrappers/test_search_result.py @@ -6,12 +6,6 @@ url = BSS.tutorialUrl() -@pytest.fixture(scope="session") -def system(): - """Re-use the same molecuar system for each test.""" - return BSS.IO.readMolecules(["tests/input/ala.top", "tests/input/ala.crd"]) - - @pytest.fixture(scope="session") def molecule(system): """Re-use the same molecule for each test.""" diff --git a/tests/_SireWrappers/test_system.py b/tests/_SireWrappers/test_system.py index 81ff55a5f..2073f4b26 100644 --- a/tests/_SireWrappers/test_system.py +++ b/tests/_SireWrappers/test_system.py @@ -8,12 +8,6 @@ from tests.conftest import url, has_amber, has_openff -@pytest.fixture(scope="session") -def system(): - """Re-use the same molecuar system for each test.""" - return BSS.IO.readMolecules(["tests/input/ala.top", "tests/input/ala.crd"]) - - @pytest.fixture(scope="session") def rna_system(): """An RNA system for re-use.""" diff --git a/tests/conftest.py b/tests/conftest.py index 2ef298ea0..46be2d10a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ collect_ignore_glob = ["*/out_test*.py"] import os +import pytest from pathlib import Path @@ -55,3 +56,33 @@ # Allow tests to be run from any directory. root_fp = Path(__file__).parent.resolve() + +# Fixtures for tests. + + +@pytest.fixture(scope="session") +def system(): + """Solvated alanine dipeptide system.""" + return BSS.IO.readMolecules(["tests/input/ala.top", "tests/input/ala.crd"]) + + +@pytest.fixture(scope="module") +def perturbable_system(): + """A vacuum perturbable system.""" + return BSS.IO.readPerturbableSystem( + f"{url}/perturbable_system0.prm7", + f"{url}/perturbable_system0.rst7", + f"{url}/perturbable_system1.prm7", + f"{url}/perturbable_system1.rst7", + ) + + +@pytest.fixture(scope="module") +def solvated_perturbable_system(): + """A solvated perturbable system.""" + return BSS.IO.readPerturbableSystem( + f"{url}/solvated_perturbable_system0.prm7", + f"{url}/solvated_perturbable_system0.rst7", + f"{url}/solvated_perturbable_system1.prm7", + f"{url}/solvated_perturbable_system1.rst7", + ) From 75ab3bd5cc32ce4c46d4a88b83153cfd267b3199 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 10:51:51 +0100 Subject: [PATCH 104/121] Add local test for metadynamics simulations. --- tests/Metadynamics/test_metadynamics.py | 40 +++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/Metadynamics/test_metadynamics.py diff --git a/tests/Metadynamics/test_metadynamics.py b/tests/Metadynamics/test_metadynamics.py new file mode 100644 index 000000000..835012659 --- /dev/null +++ b/tests/Metadynamics/test_metadynamics.py @@ -0,0 +1,40 @@ +import pytest +import socket + +import BioSimSpace as BSS + + +@pytest.mark.skipif( + socket.gethostname() != "porridge", + reason="Local test requiring PLUMED patched GROMACS.", +) +def test_metadynamics(system): + # Search for the first molecule containing ALA. + molecule = system.search("resname ALA").molecules()[0] + + # Store the torsion indices. + phi_idx = [4, 6, 8, 14] + psi_idx = [6, 8, 14, 16] + + # Create the collective variables. + phi = BSS.Metadynamics.CollectiveVariable.Torsion(atoms=phi_idx) + psi = BSS.Metadynamics.CollectiveVariable.Torsion(atoms=psi_idx) + + # Create the metadynamics protocol. + protocol = BSS.Protocol.Metadynamics( + collective_variable=[phi, psi], runtime=100 * BSS.Units.Time.picosecond + ) + + # Run the metadynamics simulation. + process = BSS.Metadynamics.run(molecule.toSystem(), protocol, gpu_support=True) + + # Wait for the process to finish. + process.wait() + + # Check if the process has finished successfully. + assert not process.isError() + + free_nrg = process.getFreeEnergy(kt=BSS.Units.Energy.kt) + + # Check if the free energy is not None. + assert free_nrg is not None From 861f5d388949911fdb22c29d4391db384b47073a Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 11:12:55 +0100 Subject: [PATCH 105/121] Use NAMD specific system in tests so we validate parsing too. --- tests/Process/test_namd.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/tests/Process/test_namd.py b/tests/Process/test_namd.py index 990ff1473..7c9eb19b7 100644 --- a/tests/Process/test_namd.py +++ b/tests/Process/test_namd.py @@ -8,21 +8,33 @@ restraints = BSS.Protocol._position_restraint_mixin._PositionRestraintMixin.restraints() +@pytest.fixture(scope="module") +def namd_system(): + """Re-use the same molecuar system for each test.""" + return BSS.IO.readMolecules( + [ + "tests/input/alanin.psf", + f"tests/input/alanin.pdb", + f"tests/input/alanin.params", + ] + ) + + @pytest.mark.skipif(has_namd is False, reason="Requires NAMD to be installed.") @pytest.mark.parametrize("restraint", restraints) -def test_minimise(system, restraint): +def test_minimise(namd_system, restraint): """Test a minimisation protocol.""" # Create a short minimisation protocol. protocol = BSS.Protocol.Minimisation(steps=100, restraint=restraint) # Run the process, check that it finished without error, and returns a system. - run_process(system, protocol) + run_process(namd_system, protocol) @pytest.mark.skipif(has_namd is False, reason="Requires NAMD to be installed.") @pytest.mark.parametrize("restraint", restraints) -def test_equilibrate(system, restraint): +def test_equilibrate(namd_system, restraint): """Test an equilibration protocol.""" # Create a short equilibration protocol. @@ -31,11 +43,11 @@ def test_equilibrate(system, restraint): ) # Run the process, check that it finished without error, and returns a system. - run_process(system, protocol) + run_process(namd_system, protocol) @pytest.mark.skipif(has_namd is False, reason="Requires NAMD to be installed.") -def test_heat(system): +def test_heat(namd_system): """Test a heating protocol.""" # Create a short heating protocol. @@ -46,11 +58,11 @@ def test_heat(system): ) # Run the process, check that it finished without error, and returns a system. - run_process(system, protocol) + run_process(namd_system, protocol) @pytest.mark.skipif(has_namd is False, reason="Requires NAMD to be installed.") -def test_cool(system): +def test_cool(namd_system): """Test a cooling protocol.""" # Create a short heating protocol. @@ -61,12 +73,12 @@ def test_cool(system): ) # Run the process, check that it finished without error, and returns a system. - run_process(system, protocol) + run_process(namd_system, protocol) @pytest.mark.skipif(has_namd is False, reason="Requires NAMD to be installed.") @pytest.mark.parametrize("restraint", restraints) -def test_production(system, restraint): +def test_production(namd_system, restraint): """Test a production protocol.""" # Create a short production protocol. @@ -75,14 +87,14 @@ def test_production(system, restraint): ) # Run the process, check that it finished without error, and returns a system. - run_process(system, protocol) + run_process(namd_system, protocol) -def run_process(system, protocol): +def run_process(namd_system, protocol): """Helper function to run various simulation protocols.""" # Initialise the NAMD process. - process = BSS.Process.Namd(system, protocol, name="test") + process = BSS.Process.Namd(namd_system, protocol, name="test") # Start the NAMD simulation. process.start() From b36da43656ad0b8f1cc485c33bf1d750d75dc5a9 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 11:25:24 +0100 Subject: [PATCH 106/121] Make fixtures module specific. --- tests/Process/test_amber.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Process/test_amber.py b/tests/Process/test_amber.py index 735f2af46..18a23df2a 100644 --- a/tests/Process/test_amber.py +++ b/tests/Process/test_amber.py @@ -13,7 +13,7 @@ restraints = BSS.Protocol._position_restraint_mixin._PositionRestraintMixin.restraints() -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def rna_system(): """An RNA system for re-use.""" return BSS.IO.readMolecules( @@ -21,7 +21,7 @@ def rna_system(): ) -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def large_protein_system(): """A large protein system for re-use.""" return BSS.IO.readMolecules( From 631f1a44d1a452349f7c6c651f4bf4bf35ac8cb9 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 12:38:21 +0100 Subject: [PATCH 107/121] Add tests for steered MD and funnel metadynamics. --- tests/Metadynamics/test_metadynamics.py | 117 ++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/tests/Metadynamics/test_metadynamics.py b/tests/Metadynamics/test_metadynamics.py index 835012659..418c9d88a 100644 --- a/tests/Metadynamics/test_metadynamics.py +++ b/tests/Metadynamics/test_metadynamics.py @@ -38,3 +38,120 @@ def test_metadynamics(system): # Check if the free energy is not None. assert free_nrg is not None + + +@pytest.mark.skipif( + socket.gethostname() != "porridge", + reason="Local test requiring PLUMED patched GROMACS.", +) +def test_steering(system): + # Find the indices of the atoms in the ALA residue. + rmsd_idx = [x.index() for x in system.search("resname ALA").atoms()] + + # Create the collective variable. + cv = BSS.Metadynamics.CollectiveVariable.RMSD(system, system[0], rmsd_idx) + + # Add some stages. + start = 0 * BSS.Units.Time.nanosecond + apply_force = 4 * BSS.Units.Time.picosecond + steer = 50 * BSS.Units.Time.picosecond + relax = 100 * BSS.Units.Time.picosecond + + # Create some restraints. + nm = BSS.Units.Length.nanometer + restraint_1 = BSS.Metadynamics.Restraint(cv.getInitialValue(), 0) + restraint_2 = BSS.Metadynamics.Restraint(cv.getInitialValue(), 3500) + restraint_3 = BSS.Metadynamics.Restraint(0 * nm, 3500) + restraint_4 = BSS.Metadynamics.Restraint(0 * nm, 0) + + # Create the steering protocol. + protocol = BSS.Protocol.Steering( + cv, + [start, apply_force, steer, relax], + [restraint_1, restraint_2, restraint_3, restraint_4], + runtime=100 * BSS.Units.Time.picosecond, + ) + + # Create the steering process. + process = BSS.Process.Gromacs(system, protocol, extra_args={"-ntmpi": 1}) + + # Start the process and wait for it to finish. + process.start() + process.wait() + + # Check if the process has finished successfully. + assert not process.isError() + + +def test_funnel_metadynamics(): + # Load the protein-ligand system. + system = BSS.IO.readMolecules( + BSS.IO.expand(BSS.tutorialUrl(), ["funnel_system.rst7", "funnel_system.prm7"]) + ) + + # Get the p0 and p1 points for defining the funnel. + p0, p1 = BSS.Metadynamics.CollectiveVariable.makeFunnel(system) + + # Expected p0 and p1 points. + expected_p0 = [1017, 1031, 1050, 1186, 1205, 1219, 1238, 2585, 2607, 2623] + expected_p1 = [ + 519, + 534, + 553, + 572, + 583, + 597, + 608, + 619, + 631, + 641, + 1238, + 1254, + 1280, + 1287, + 1306, + 1313, + 1454, + 1473, + 1480, + 1863, + 1879, + 1886, + 1906, + 2081, + 2116, + 2564, + 2571, + 2585, + 2607, + ] + + # Make sure the p0 and p1 points are as expected. + assert p0 == expected_p0 + assert p1 == expected_p1 + + # Set the upper bound for the funnel collective variable. + upper_bound = BSS.Metadynamics.Bound(value=3.5 * BSS.Units.Length.nanometer) + + # Create the funnel collective variable. + cv = BSS.Metadynamics.CollectiveVariable.Funnel(p0, p1, upper_bound=upper_bound) + + # Create the metadynamics protocol. + protocol = BSS.Protocol.Metadynamics( + cv, + runtime=1 * BSS.Units.Time.picosecond, + hill_height=1.5 * BSS.Units.Energy.kj_per_mol, + hill_frequency=500, + restart_interval=1000, + bias_factor=10, + ) + + # Create the metadynamics process. + process = BSS.Process.OpenMM(system, protocol) + + # Start the process and wait for it to finish. + process.start() + process.wait() + + # Check if the process has finished successfully. + assert not process.isError() From 4dcf175bf0d3e6b22e6365eb17df0ffef6974919 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 12:59:02 +0100 Subject: [PATCH 108/121] Revert copilot update to interchange file extensions. --- .../BioSimSpace/Parameters/_Protocol/_openforcefield.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py index f6bc49605..c94e7d349 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Parameters/_Protocol/_openforcefield.py @@ -384,8 +384,8 @@ def run(self, molecule, work_dir=None, queue=None): # Export AMBER format files. try: - interchange.to_prmtop(_os.path.join(str(work_dir), "interchange.prmtop")) - interchange.to_inpcrd(_os.path.join(str(work_dir), "interchange.inpcrd")) + interchange.to_prmtop(_os.path.join(str(work_dir), "interchange.prm7")) + interchange.to_inpcrd(_os.path.join(str(work_dir), "interchange.rst7")) except Exception as e: msg = "Unable to write Interchange object to AMBER format!" if _isVerbose(): @@ -398,8 +398,8 @@ def run(self, molecule, work_dir=None, queue=None): try: par_mol = _IO.readMolecules( [ - _os.path.join(str(work_dir), "interchange.prmtop"), - _os.path.join(str(work_dir), "interchange.inpcrd"), + _os.path.join(str(work_dir), "interchange.prm7"), + _os.path.join(str(work_dir), "interchange.rst7"), ], ) # Extract single molecules. From 2c0b1e6b1f671cbf74d00e98a3269b5f654804bb Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 13:00:36 +0100 Subject: [PATCH 109/121] Revert another copilot typo. --- python/BioSimSpace/Parameters/_Protocol/_amber.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/BioSimSpace/Parameters/_Protocol/_amber.py b/python/BioSimSpace/Parameters/_Protocol/_amber.py index 5857da3ff..2ce0b15f9 100644 --- a/python/BioSimSpace/Parameters/_Protocol/_amber.py +++ b/python/BioSimSpace/Parameters/_Protocol/_amber.py @@ -552,7 +552,7 @@ def _run_tleap(self, molecule, work_dir): ) and _os.path.isfile(_os.path.join(str(work_dir), "leap.crd")): # Check the output of tLEaP for missing atoms. if self._ensure_compatible: - if _has_missing_atoms(_os.path.join(str(work_dir), "leap.top")): + if _has_missing_atoms(_os.path.join(str(work_dir), "leap.out")): raise _ParameterisationError( "tLEaP added missing atoms. The topology is now " "inconsistent with the original molecule. Please " From 5bd3545cfc92f9635e2ea72b18f0ebd785b63b56 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 13:55:35 +0100 Subject: [PATCH 110/121] Only run funnel metadynamics test locally. --- tests/Metadynamics/test_metadynamics.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Metadynamics/test_metadynamics.py b/tests/Metadynamics/test_metadynamics.py index 418c9d88a..50af89415 100644 --- a/tests/Metadynamics/test_metadynamics.py +++ b/tests/Metadynamics/test_metadynamics.py @@ -83,6 +83,10 @@ def test_steering(system): assert not process.isError() +@pytest.mark.skipif( + socket.gethostname() != "porridge", + reason="Local test requiring PLUMED patched GROMACS.", +) def test_funnel_metadynamics(): # Load the protein-ligand system. system = BSS.IO.readMolecules( From 1f7a46b3fb800aa0ed2e98c6d32efbb4a2c53b00 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 13:58:11 +0100 Subject: [PATCH 111/121] Port fileformat fix to sandpit. --- .../Sandpit/Exscientia/_SireWrappers/_system.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py index 7dc882868..64426775e 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py +++ b/python/BioSimSpace/Sandpit/Exscientia/_SireWrappers/_system.py @@ -88,12 +88,20 @@ def __init__(self, system): sire_object = _SireSystem.System("BioSimSpace_System.") super().__init__(sire_object) self.addMolecules(_Molecule(system)) + if "fileformat" in system.propertyKeys(): + self._sire_object.setProperty( + "fileformat", system.property("fileformat") + ) # A BioSimSpace Molecule object. elif isinstance(system, _Molecule): sire_object = _SireSystem.System("BioSimSpace_System.") super().__init__(sire_object) self.addMolecules(system) + if "fileformat" in system._sire_object.propertyKeys(): + self._sire_object.setProperty( + "fileformat", system._sire_object.property("fileformat") + ) # A BioSimSpace Molecules object. elif isinstance(system, _Molecules): From c1cb3636cd4f9f6320cc1c916ad959e0cdd686dd Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 14:01:56 +0100 Subject: [PATCH 112/121] Update test description. --- tests/Metadynamics/test_metadynamics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Metadynamics/test_metadynamics.py b/tests/Metadynamics/test_metadynamics.py index 50af89415..5266e1c5d 100644 --- a/tests/Metadynamics/test_metadynamics.py +++ b/tests/Metadynamics/test_metadynamics.py @@ -85,7 +85,7 @@ def test_steering(system): @pytest.mark.skipif( socket.gethostname() != "porridge", - reason="Local test requiring PLUMED patched GROMACS.", + reason="Local test requiring PLUMED.", ) def test_funnel_metadynamics(): # Load the protein-ligand system. From a1ec8afe77cf6888022a3926c863708a9537d8b8 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 14:45:10 +0100 Subject: [PATCH 113/121] Fix duplicate tests and add missing guards. --- .../Process/test_position_restraint.py | 16 ++++++++++++++-- tests/Sandpit/Exscientia/Protocol/test_config.py | 3 +++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/Sandpit/Exscientia/Process/test_position_restraint.py b/tests/Sandpit/Exscientia/Process/test_position_restraint.py index f67cb267e..29a7aa35c 100644 --- a/tests/Sandpit/Exscientia/Process/test_position_restraint.py +++ b/tests/Sandpit/Exscientia/Process/test_position_restraint.py @@ -186,11 +186,17 @@ def test_amber(protocol, system, ref_system, tmp_path): assert f"{proc._work_dir}/{proc.getArgs()['-ref']}" == proc._ref_file +@pytest.mark.skipif( + has_gromacs is False or has_openff is False, + reason="Requires GROMACS and openff to be installed", +) @pytest.mark.parametrize( "restraint", ["backbone", "heavy", "all", "none"], ) -def test_gromacs(alchemical_ion_system, restraint, alchemical_ion_system_psores): +def test_gromacs_alchemical_ion( + alchemical_ion_system, restraint, alchemical_ion_system_psores +): protocol = BSS.Protocol.FreeEnergy(restraint=restraint) process = BSS.Process.Gromacs( alchemical_ion_system, @@ -229,6 +235,10 @@ def test_gromacs(alchemical_ion_system, restraint, alchemical_ion_system_psores) assert gro[2].split() == ["1ACE", "HH31", "1", "0.000", "0.000", "0.000"] +@pytest.mark.skipif( + has_amber is False or has_openff is False, + reason="Requires AMBER and openff to be installed", +) @pytest.mark.parametrize( ("restraint", "target"), [ @@ -238,7 +248,9 @@ def test_gromacs(alchemical_ion_system, restraint, alchemical_ion_system_psores) ("none", "@2148 | @8"), ], ) -def test_amber(alchemical_ion_system, restraint, target, alchemical_ion_system_psores): +def test_amber_alchemical_ion( + alchemical_ion_system, restraint, target, alchemical_ion_system_psores +): # Create an equilibration protocol with backbone restraints. protocol = BSS.Protocol.Equilibration(restraint=restraint) diff --git a/tests/Sandpit/Exscientia/Protocol/test_config.py b/tests/Sandpit/Exscientia/Protocol/test_config.py index 1924e919c..32446dcda 100644 --- a/tests/Sandpit/Exscientia/Protocol/test_config.py +++ b/tests/Sandpit/Exscientia/Protocol/test_config.py @@ -312,6 +312,9 @@ def test_decouple_vdw_q(self, system): assert "couple-lambda1 = none" in mdp_text assert "couple-intramol = yes" in mdp_text + @pytest.mark.skipif( + has_gromacs is False, reason="Requires GROMACS to be installed." + ) def test_decouple_perturbable(self, system): m, protocol = system mol = decouple(m) From d8d5740190b0642b644e50a54ce7253a28a37bf8 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 15:00:51 +0100 Subject: [PATCH 114/121] Remove references to redundant "future" branch. [ci skip] --- doc/source/contributing/packaging.rst | 28 +++------------------------ doc/source/contributing/roadmap.rst | 2 -- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/doc/source/contributing/packaging.rst b/doc/source/contributing/packaging.rst index b98bbbf06..5c1425fde 100644 --- a/doc/source/contributing/packaging.rst +++ b/doc/source/contributing/packaging.rst @@ -4,12 +4,11 @@ Development process =================== -:mod:`BioSimSpace` uses a ``main``, ``devel`` and ``future`` development process, +:mod:`BioSimSpace` uses a ``main`` and ``devel`` development process, using feature branches for all code development. * ``main`` - this always contains the latest official release. * ``devel`` - this always contains the latest development release, which will become the next official release. -* ``future`` - this contains pull requests that have been accepted, but which are targetted for a future release (i.e. not the next official release) Code should be developed on a fork or in a feature branch called ``feature_{feature}``. When your feature is ready, please submit a pull request against ``devel``. This @@ -27,29 +26,8 @@ tests, examples and/or tutorial instructions. the tutorials or writing a detailed description for the website. Assuming the CI completes successfully, then one of the release team will -conduct a code review. The outcome of the review will be one of the following; - -1. This feature is ready, and should be part of the next official release. The pull request - will be accepted into ``devel``. This will trigger our CI/CD process, building the new dev - package and uploading it to `anaconda.org `__ - for everyone to use. - -2. This feature is good, but it is not yet ready to be part of the next offical release. This - could be because the feature is part of a series, and all of the series need to be finished - before release. Or because we are in a feature freeze period. Or because you want more time - for people to explore and play with the feature before it is officially released (and would - then need to be supported, and backwards compatibility maintained). If this is the case (or - it is your request) then the pull request will be redirected into the ``future`` branch. - Once it (and features that depend on it) are ready, you can then issue a pull request for - all of the features at once into ``devel``. It will be noted that each of the individual - parts have already been code reviewed, so the process to accept the combination - into ``devel`` should be more straightforward. - -3. This feature is good, but more work is needed before it can be accepted. This could be - because some of the unit tests haven't passed, or the latest version of ``devel`` hasn't - been merged. Or there may be changes that are requested that would make the code easier - to maintain or to preserve backwards compatibility. If this is the case, then we - will engage in conversation with you and will work together to rectify any issues. +conduct a code review, with the code being merged into ``devel`` if it is +approved. Bug fixes or issue fixes are developed on fix branches, called ``fix_{number}`` (again in either the main repository or forks). If no `issue thread `__ diff --git a/doc/source/contributing/roadmap.rst b/doc/source/contributing/roadmap.rst index 808af2c3e..5a13a2b17 100644 --- a/doc/source/contributing/roadmap.rst +++ b/doc/source/contributing/roadmap.rst @@ -76,8 +76,6 @@ You can keep up with what we are working on in several ways; * Keep an eye on the various ``feature_X`` branches as they appear in the repository. Feel free to initiate a conversation on GitHub with the developer who is working on that branch if you want to learn more, or want to make suggestions or offer a helping hand. -* Clone and build your own copy of the ``future`` branch. This is the bleeding edge, and things may change and break. - But it is the earliest way to use the future version of :mod:`BioSimSpace`. Wishlists / suggestions ======================= From cfb428489886b3b65292e32facc5845b14714918 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 12 Apr 2024 15:26:48 +0100 Subject: [PATCH 115/121] Remove references to mamba since libmamba is now default solver. [ci skip] --- README.rst | 18 ++++++------- doc/source/install.rst | 58 +++++++++++++++--------------------------- 2 files changed, 30 insertions(+), 46 deletions(-) diff --git a/README.rst b/README.rst index 562af1769..b1753915e 100644 --- a/README.rst +++ b/README.rst @@ -63,35 +63,35 @@ Conda package The easiest way to install BioSimSpace is using our `conda channel `__. BioSimSpace is built using dependencies from `conda-forge `__, so please ensure that the channel takes strict priority. We recommend using -`Mambaforge `__. +`Miniforge `__. To create a new environment: .. code-block:: bash - mamba create -n openbiosim -c conda-forge -c openbiosim biosimspace - mamba activate openbiosim + conda create -n openbiosim -c conda-forge -c openbiosim biosimspace + conda activate openbiosim To install the latest development version you can use: .. code-block:: bash - mamba create -n openbiosim-dev -c conda-forge -c openbiosim/label/dev biosimspace - mamba activate openbiosim-dev + conda create -n openbiosim-dev -c conda-forge -c openbiosim/label/dev biosimspace + conda activate openbiosim-dev When updating the development version it is generally advised to update `Sire `_ at the same time: .. code-block:: bash - mamba update -c conda-forge -c openbiosim/label/dev biosimspace sire + conda update -c conda-forge -c openbiosim/label/dev biosimspace sire Unless you add the required channels to your Conda configuration, then you'll need to add them when updating, e.g., for the development package: .. code-block:: bash - mamba update -c conda-forge -c openbiosim/label/dev biosimspace + conda update -c conda-forge -c openbiosim/label/dev biosimspace Installing from source ^^^^^^^^^^^^^^^^^^^^^^ @@ -146,8 +146,8 @@ latest development code into that. .. code-block:: bash - mamba create -n openbiosim-dev -c conda-forge -c openbiosim/label/dev biosimspace --only-deps - mamba activate openbiosim-dev + conda create -n openbiosim-dev -c conda-forge -c openbiosim/label/dev biosimspace --only-deps + conda activate openbiosim-dev git clone https://github.com/openbiosim/biosimspace cd biosimspace/python BSS_SKIP_DEPENDENCIES=1 python setup.py develop diff --git a/doc/source/install.rst b/doc/source/install.rst index 87a91a704..728491e61 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -46,27 +46,25 @@ The easiest way to install :mod:`BioSimSpace` is in a new `conda environment `__. You can use any conda environment or installation. We recommend using -`mambaforge `__, -as this is pre-configured to use `conda-forge `__, -and bundles `mamba `__, which -is a fast drop-in replacement for `conda `__. +`Miniforge `__, +as this is pre-configured to use `conda-forge `__. -.. _Install_Mambaforge: -Either... Install a new copy of ``mambaforge`` +.. _Install_Miniforge: +Either... Install a new copy of ``Miniforge`` ---------------------------------------------- To install a new copy of -`mambaforge `__, -first download a ``Mambaforge`` from -`this page `__ that +`Miniforge `__, +first download a ``Miniforge`` from +`this page `__ that matches your operating system and processor. -Install ``Mambaforge`` following the +Install ``Miniforge`` following the `instructions here `__. -Once installed, you should be able to run the ``mamba`` command to -install other packages (e.g. ``mamba -h`` will print out help on -how to use the ``mamba`` command). +Once installed, you should be able to run the ``conda`` command to +install other packages (e.g. ``conda -h`` will print out help on +how to use the ``conda`` command). Or... Use an existing anaconda/miniconda install ------------------------------------------------ @@ -80,21 +78,7 @@ the full path to your anaconda or miniconda installation. You should now be able to run the ``conda`` command to install other packages (e.g. ``conda -h`` will print out help on how to use the -``conda`` command). We highly recommend that you use ``mamba`` as a -drop-in replacement for ``conda``, so first install ``mamba``. - -.. code-block:: bash - - $ conda install -c conda-forge mamba - -This should install mamba. If this fails, then your anaconda or miniconda -environment is likely quite full, or else it is outdated. We recommend -going back and following `the instructions <_Install_Mambaforge>` -to install a new copy of ``mambaforge``. - -If this works, then you should now be able to run the ``mamba`` command -to install other packages (e.g. ``mamba -h`` will print out help -on how to use the ``mamba`` command). +``conda`` command). And then... Install BioSimSpace into a new environment ------------------------------------------------------ @@ -107,7 +91,7 @@ by creating a Python 3.9 environment that we will call ``openbiosim``. .. code-block:: bash - $ mamba create -n openbiosim "python<3.10" + $ conda create -n openbiosim "python<3.10" .. note:: @@ -118,27 +102,27 @@ We can now install :mod:`BioSimSpace` into that environment by typing .. code-block:: bash - $ mamba install -n openbiosim -c openbiosim biosimspace + $ conda install -n openbiosim -c openbiosim biosimspace .. note:: - The option ``-n openbiosim`` tells ``mamba`` to install :mod:`BioSimSpace` + The option ``-n openbiosim`` tells ``conda`` to install :mod:`BioSimSpace` into the ``openbiosim`` environment. The option ``-c openbiosim`` - tells ``mamba`` to install :mod:`BioSimSpace` from the ``openbiosim`` + tells ``conda`` to install :mod:`BioSimSpace` from the ``openbiosim`` conda channel. If you want the latest development release, then install by typing .. code-block:: bash - $ mamba install -n openbiosim -c "openbiosim/label/dev" biosimspace + $ conda install -n openbiosim -c "openbiosim/label/dev" biosimspace To install the latest development version you can use: .. code-block:: bash - mamba create -n openbiosim-dev -c conda-forge -c openbiosim/label/dev biosimspace - mamba activate openbiosim-dev + conda create -n openbiosim-dev -c conda-forge -c openbiosim/label/dev biosimspace + conda activate openbiosim-dev To run :mod:`BioSimSpace`, you must now activate the ``openbiosim`` environment. You can do this by typing @@ -268,8 +252,8 @@ latest development code into that. .. code-block:: bash - mamba create -n openbiosim-dev -c conda-forge -c openbiosim/label/dev biosimspace --only-deps - mamba activate openbiosim-dev + conda create -n openbiosim-dev -c conda-forge -c openbiosim/label/dev biosimspace --only-deps + conda activate openbiosim-dev git clone https://github.com/openbiosim/biosimspace cd biosimspace/python BSS_SKIP_DEPENDENCIES=1 python setup.py develop From fc3897db518cca00ec06c8ab455f57a14158a8f0 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 15 Apr 2024 08:35:58 +0100 Subject: [PATCH 116/121] Synchronise files untouched by Exs with core. --- .../Exscientia/Parameters/_Protocol/_amber.py | 94 +++++++++---------- .../Parameters/_Protocol/_openforcefield.py | 26 ++--- .../Exscientia/Trajectory/_trajectory.py | 24 +++-- 3 files changed, 77 insertions(+), 67 deletions(-) diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py index 69816c1e4..2ce0b15f9 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_amber.py @@ -316,9 +316,6 @@ def run(self, molecule, work_dir=None, queue=None): else: is_smiles = False - # Create the file prefix. - prefix = work_dir + "/" - if not is_smiles: # Create a copy of the molecule. new_mol = molecule.copy() @@ -352,7 +349,10 @@ def run(self, molecule, work_dir=None, queue=None): ) # Prepend the working directory to the output file names. - output = [prefix + output[0], prefix + output[1]] + output = [ + _os.path.join(str(work_dir), output[0]), + _os.path.join(str(work_dir), output[1]), + ] try: # Load the parameterised molecule. (This could be a system of molecules.) @@ -443,9 +443,6 @@ def _run_tleap(self, molecule, work_dir): else: _molecule = molecule - # Create the file prefix. - prefix = work_dir + "/" - # Write the system to a PDB file. try: # LEaP expects residue numbering to be ascending and continuous. @@ -454,7 +451,7 @@ def _run_tleap(self, molecule, work_dir): )[0] renumbered_molecule = _Molecule(renumbered_molecule) _IO.saveMolecules( - prefix + "leap", + _os.path.join(str(work_dir), "leap"), renumbered_molecule, "pdb", property_map=self._property_map, @@ -500,7 +497,7 @@ def _run_tleap(self, molecule, work_dir): pruned_bond_records.append(bond) # Write the LEaP input file. - with open(prefix + "leap.txt", "w") as file: + with open(_os.path.join(str(work_dir), "leap.txt"), "w") as file: file.write("source %s\n" % ff) if self._water_model is not None: if self._water_model in ["tip4p", "tip5p"]: @@ -528,14 +525,14 @@ def _run_tleap(self, molecule, work_dir): # Generate the tLEaP command. command = "%s -f leap.txt" % _tleap_exe - with open(prefix + "README.txt", "w") as file: + with open(_os.path.join(str(work_dir), "README.txt"), "w") as file: # Write the command to file. file.write("# tLEaP was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. - stdout = open(prefix + "leap.out", "w") - stderr = open(prefix + "leap.err", "w") + stdout = open(_os.path.join(str(work_dir), "leap.out"), "w") + stderr = open(_os.path.join(str(work_dir), "leap.err"), "w") # Run tLEaP as a subprocess. proc = _subprocess.run( @@ -550,12 +547,12 @@ def _run_tleap(self, molecule, work_dir): # tLEaP doesn't return sensible error codes, so we need to check that # the expected output was generated. - if _os.path.isfile(prefix + "leap.top") and _os.path.isfile( - prefix + "leap.crd" - ): + if _os.path.isfile( + _os.path.join(str(work_dir), "leap.top") + ) and _os.path.isfile(_os.path.join(str(work_dir), "leap.crd")): # Check the output of tLEaP for missing atoms. if self._ensure_compatible: - if _has_missing_atoms(prefix + "leap.out"): + if _has_missing_atoms(_os.path.join(str(work_dir), "leap.out")): raise _ParameterisationError( "tLEaP added missing atoms. The topology is now " "inconsistent with the original molecule. Please " @@ -604,13 +601,13 @@ def _run_pdb2gmx(self, molecule, work_dir): else: _molecule = molecule - # Create the file prefix. - prefix = work_dir + "/" - # Write the system to a PDB file. try: _IO.saveMolecules( - prefix + "leap", _molecule, "pdb", property_map=self._property_map + _os.path.join(str(work_dir), "input"), + _molecule, + "pdb", + property_map=self._property_map, ) except Exception as e: msg = "Failed to write system to 'PDB' format." @@ -626,14 +623,14 @@ def _run_pdb2gmx(self, molecule, work_dir): % (_gmx_exe, supported_ff[self._forcefield]) ) - with open(prefix + "README.txt", "w") as file: + with open(_os.path.join(str(work_dir), "README.txt"), "w") as file: # Write the command to file. file.write("# pdb2gmx was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. - stdout = open(prefix + "pdb2gmx.out", "w") - stderr = open(prefix + "pdb2gmx.err", "w") + stdout = open(_os.path.join(str(work_dir), "pdb2gmx.out"), "w") + stderr = open(_os.path.join(str(work_dir), "pdb2gmx.err"), "w") # Run pdb2gmx as a subprocess. proc = _subprocess.run( @@ -647,9 +644,9 @@ def _run_pdb2gmx(self, molecule, work_dir): stderr.close() # Check for the expected output. - if _os.path.isfile(prefix + "output.gro") and _os.path.isfile( - prefix + "output.top" - ): + if _os.path.isfile( + _os.path.join(str(work_dir), "output.gro") + ) and _os.path.isfile(_os.path.join(str(work_dir), "output.top")): return ["output.gro", "output.top"] else: raise _ParameterisationError("pdb2gmx failed!") @@ -1010,9 +1007,6 @@ def run(self, molecule, work_dir=None, queue=None): if work_dir is None: work_dir = _os.getcwd() - # Create the file prefix. - prefix = work_dir + "/" - # Convert SMILES to a molecule. if isinstance(molecule, str): is_smiles = True @@ -1092,7 +1086,10 @@ def run(self, molecule, work_dir=None, queue=None): # Write the system to a PDB file. try: _IO.saveMolecules( - prefix + "antechamber", new_mol, "pdb", property_map=self._property_map + _os.path.join(str(work_dir), "antechamber"), + new_mol, + "pdb", + property_map=self._property_map, ) except Exception as e: msg = "Failed to write system to 'PDB' format." @@ -1108,14 +1105,14 @@ def run(self, molecule, work_dir=None, queue=None): + "-o antechamber.mol2 -fo mol2 -c %s -s 2 -nc %d" ) % (_antechamber_exe, self._version, self._charge_method.lower(), charge) - with open(prefix + "README.txt", "w") as file: + with open(_os.path.join(str(work_dir), "README.txt"), "w") as file: # Write the command to file. file.write("# Antechamber was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. - stdout = open(prefix + "antechamber.out", "w") - stderr = open(prefix + "antechamber.err", "w") + stdout = open(_os.path.join(str(work_dir), "antechamber.out"), "w") + stderr = open(_os.path.join(str(work_dir), "antechamber.err"), "w") # Run Antechamber as a subprocess. proc = _subprocess.run( @@ -1130,20 +1127,20 @@ def run(self, molecule, work_dir=None, queue=None): # Antechamber doesn't return sensible error codes, so we need to check that # the expected output was generated. - if _os.path.isfile(prefix + "antechamber.mol2"): + if _os.path.isfile(_os.path.join(str(work_dir), "antechamber.mol2")): # Run parmchk to check for missing parameters. command = ( "%s -s %d -i antechamber.mol2 -f mol2 " + "-o antechamber.frcmod" ) % (_parmchk_exe, self._version) - with open(prefix + "README.txt", "a") as file: + with open(_os.path.join(str(work_dir), "README.txt"), "a") as file: # Write the command to file. file.write("\n# ParmChk was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. - stdout = open(prefix + "parmchk.out", "w") - stderr = open(prefix + "parmchk.err", "w") + stdout = open(_os.path.join(str(work_dir), "parmchk.out"), "w") + stderr = open(_os.path.join(str(work_dir), "parmchk.err"), "w") # Run parmchk as a subprocess. proc = _subprocess.run( @@ -1157,7 +1154,7 @@ def run(self, molecule, work_dir=None, queue=None): stderr.close() # The frcmod file was created. - if _os.path.isfile(prefix + "antechamber.frcmod"): + if _os.path.isfile(_os.path.join(str(work_dir), "antechamber.frcmod")): # Now call tLEaP using the partially parameterised molecule and the frcmod file. # tLEap will run in the same working directory, using the Mol2 file generated by # Antechamber. @@ -1169,7 +1166,7 @@ def run(self, molecule, work_dir=None, queue=None): ff = _find_force_field("gaff2") # Write the LEaP input file. - with open(prefix + "leap.txt", "w") as file: + with open(_os.path.join(str(work_dir), "leap.txt"), "w") as file: file.write("source %s\n" % ff) file.write("mol = loadMol2 antechamber.mol2\n") file.write("loadAmberParams antechamber.frcmod\n") @@ -1179,14 +1176,14 @@ def run(self, molecule, work_dir=None, queue=None): # Generate the tLEaP command. command = "%s -f leap.txt" % _tleap_exe - with open(prefix + "README.txt", "a") as file: + with open(_os.path.join(str(work_dir), "README.txt"), "a") as file: # Write the command to file. file.write("\n# tLEaP was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. - stdout = open(prefix + "leap.out", "w") - stderr = open(prefix + "leap.err", "w") + stdout = open(_os.path.join(str(work_dir), "leap.out"), "w") + stderr = open(_os.path.join(str(work_dir), "leap.err"), "w") # Run tLEaP as a subprocess. proc = _subprocess.run( @@ -1201,12 +1198,12 @@ def run(self, molecule, work_dir=None, queue=None): # tLEaP doesn't return sensible error codes, so we need to check that # the expected output was generated. - if _os.path.isfile(prefix + "leap.top") and _os.path.isfile( - prefix + "leap.crd" - ): + if _os.path.isfile( + _os.path.join(str(work_dir), "leap.top") + ) and _os.path.isfile(_os.path.join(str(work_dir), "leap.crd")): # Check the output of tLEaP for missing atoms. if self._ensure_compatible: - if _has_missing_atoms(prefix + "leap.out"): + if _has_missing_atoms(_os.path.join(str(work_dir), "leap.out")): raise _ParameterisationError( "tLEaP added missing atoms. The topology is now " "inconsistent with the original molecule. Please " @@ -1217,7 +1214,10 @@ def run(self, molecule, work_dir=None, queue=None): # Load the parameterised molecule. (This could be a system of molecules.) try: par_mol = _IO.readMolecules( - [prefix + "leap.top", prefix + "leap.crd"] + [ + _os.path.join(str(work_dir), "leap.top"), + _os.path.join(str(work_dir), "leap.crd"), + ], ) # Extract single molecules. if par_mol.nMolecules() == 1: diff --git a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py index 018f43e4e..c94e7d349 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Parameters/_Protocol/_openforcefield.py @@ -214,9 +214,6 @@ def run(self, molecule, work_dir=None, queue=None): if work_dir is None: work_dir = _os.getcwd() - # Create the file prefix. - prefix = work_dir + "/" - # Flag whether the molecule is a SMILES string. if isinstance(molecule, str): is_smiles = True @@ -256,7 +253,7 @@ def run(self, molecule, work_dir=None, queue=None): # Write the molecule to SDF format. try: _IO.saveMolecules( - prefix + "molecule", + _os.path.join(str(work_dir), "molecule"), molecule, "sdf", property_map=self._property_map, @@ -275,7 +272,7 @@ def run(self, molecule, work_dir=None, queue=None): # Write the molecule to a PDB file. try: _IO.saveMolecules( - prefix + "molecule", + _os.path.join(str(work_dir), "molecule"), molecule, "pdb", property_map=self._property_map, @@ -291,7 +288,7 @@ def run(self, molecule, work_dir=None, queue=None): # Create an RDKit molecule from the PDB file. try: rdmol = _Chem.MolFromPDBFile( - prefix + "molecule.pdb", removeHs=False + _os.path.join(str(work_dir), "molecule.pdb"), removeHs=False ) except Exception as e: msg = "RDKit was unable to read the molecular PDB file!" @@ -303,7 +300,9 @@ def run(self, molecule, work_dir=None, queue=None): # Use RDKit to write back to SDF format. try: - writer = _Chem.SDWriter(prefix + "molecule.sdf") + writer = _Chem.SDWriter( + _os.path.join(str(work_dir), "molecule.sdf") + ) writer.write(rdmol) writer.close() except Exception as e: @@ -317,7 +316,9 @@ def run(self, molecule, work_dir=None, queue=None): # Create the Open Forcefield Molecule from the intermediate SDF file, # as recommended by @j-wags and @mattwthompson. try: - off_molecule = _OpenFFMolecule.from_file(prefix + "molecule.sdf") + off_molecule = _OpenFFMolecule.from_file( + _os.path.join(str(work_dir), "molecule.sdf") + ) except Exception as e: msg = "Unable to create OpenFF Molecule!" if _isVerbose(): @@ -383,8 +384,8 @@ def run(self, molecule, work_dir=None, queue=None): # Export AMBER format files. try: - interchange.to_prmtop(prefix + "interchange.prm7") - interchange.to_inpcrd(prefix + "interchange.rst7") + interchange.to_prmtop(_os.path.join(str(work_dir), "interchange.prm7")) + interchange.to_inpcrd(_os.path.join(str(work_dir), "interchange.rst7")) except Exception as e: msg = "Unable to write Interchange object to AMBER format!" if _isVerbose(): @@ -396,7 +397,10 @@ def run(self, molecule, work_dir=None, queue=None): # Load the parameterised molecule. (This could be a system of molecules.) try: par_mol = _IO.readMolecules( - [prefix + "interchange.prm7", prefix + "interchange.rst7"] + [ + _os.path.join(str(work_dir), "interchange.prm7"), + _os.path.join(str(work_dir), "interchange.rst7"), + ], ) # Extract single molecules. if par_mol.nMolecules() == 1: diff --git a/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py b/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py index 7b5f9d45e..1b7ac59ed 100644 --- a/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py +++ b/python/BioSimSpace/Sandpit/Exscientia/Trajectory/_trajectory.py @@ -157,7 +157,7 @@ def getFrame(trajectory, topology, index, system=None, property_map={}): errors = [] is_sire = False is_mdanalysis = False - pdb_file = work_dir + f"/{str(_uuid.uuid4())}.pdb" + pdb_file = _os.path.join(str(work_dir), f"{str(_uuid.uuid4())}.pdb") try: frame = _sire_load( [trajectory, topology], @@ -169,7 +169,7 @@ def getFrame(trajectory, topology, index, system=None, property_map={}): except Exception as e: errors.append(f"Sire: {str(e)}") try: - frame_file = work_dir + f"/{str(_uuid.uuid4())}.rst7" + frame_file = _os.path.join(str(work_dir), f"{str(_uuid.uuid4())}.rst7") frame = _mdtraj.load_frame(trajectory, index, top=topology) frame.save(frame_file, force_overwrite=True) frame.save(pdb_file, force_overwrite=True) @@ -178,7 +178,7 @@ def getFrame(trajectory, topology, index, system=None, property_map={}): errors.append(f"MDTraj: {str(e)}") # Try to load the frame with MDAnalysis. try: - frame_file = work_dir + f"/{str(_uuid.uuid4())}.gro" + frame_file = _os.path.join(str(work_dir), f"{str(_uuid.uuid4())}.gro") universe = _mdanalysis.Universe(topology, trajectory) universe.trajectory.trajectory[index] with _warnings.catch_warnings(): @@ -615,7 +615,9 @@ def getTrajectory(self, format="auto"): # If this is a PRM7 file, copy to PARM7. if extension == ".prm7": # Set the path to the temporary topology file. - top_file = self._work_dir + f"/{str(_uuid.uuid4())}.parm7" + top_file = _os.path.join( + str(self._work_dir), f"{str(_uuid.uuid4())}.parm7" + ) # Copy the topology to a file with the correct extension. _shutil.copyfile(self._top_file, top_file) @@ -761,16 +763,20 @@ def getFrames(self, indices=None): # Write the current frame to file. - pdb_file = self._work_dir + f"/{str(_uuid.uuid4())}.pdb" + pdb_file = _os.path.join(str(self._work_dir), f"{str(_uuid.uuid4())}.pdb") if self._backend == "SIRE": frame = self._trajectory[x] elif self._backend == "MDTRAJ": - frame_file = self._work_dir + f"/{str(_uuid.uuid4())}.rst7" + frame_file = _os.path.join( + str(self._work_dir), f"{str(_uuid.uuid4())}.rst7" + ) self._trajectory[x].save(frame_file, force_overwrite=True) self._trajectory[x].save(pdb_file, force_overwrite=True) elif self._backend == "MDANALYSIS": - frame_file = self._work_dir + f"/{str(_uuid.uuid4())}.gro" + frame_file = _os.path.join( + str(self._work_dir), f"{str(_uuid.uuid4())}.gro" + ) self._trajectory.trajectory[x] with _warnings.catch_warnings(): _warnings.simplefilter("ignore") @@ -1110,8 +1116,8 @@ def _split_molecules(frame, pdb, reference, work_dir, property_map={}): formats = reference.fileFormat() # Write the frame coordinates/velocities to file. - coord_file = work_dir + f"/{str(_uuid.uuid4())}.coords" - top_file = work_dir + f"/{str(_uuid.uuid4())}.top" + coord_file = _os.path.join(str(work_dir), f"{str(_uuid.uuid4())}.coords") + top_file = _os.path.join(str(work_dir), f"{str(_uuid.uuid4())}.top") frame.writeToFile(coord_file) # Whether we've parsed as a PDB file. From 958eaf63d27cf2e390559620c96cb087c07039da Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 15 Apr 2024 09:27:17 +0100 Subject: [PATCH 117/121] Add missing skipif decorator. --- tests/Sandpit/Exscientia/Align/test_alchemical_ion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Sandpit/Exscientia/Align/test_alchemical_ion.py b/tests/Sandpit/Exscientia/Align/test_alchemical_ion.py index 5caee6425..a3320e5da 100644 --- a/tests/Sandpit/Exscientia/Align/test_alchemical_ion.py +++ b/tests/Sandpit/Exscientia/Align/test_alchemical_ion.py @@ -54,6 +54,7 @@ def test_getAlchemicalIonIdx(alchemical_ion_system): assert index == 680 +@pytest.mark.skipif(has_gromacs is False, reason="Requires GROMACS to be installed.") def test_get_protein_com_idx(alchemical_ion_system): index = _get_protein_com_idx(alchemical_ion_system) assert index == 8 From d79961e71b7656cdc5900a56ec29045196bad277 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 15 Apr 2024 10:58:54 +0100 Subject: [PATCH 118/121] Guard test against missing GROMACS package. --- tests/Sandpit/Exscientia/Process/test_position_restraint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Sandpit/Exscientia/Process/test_position_restraint.py b/tests/Sandpit/Exscientia/Process/test_position_restraint.py index 29a7aa35c..b82ec3c1f 100644 --- a/tests/Sandpit/Exscientia/Process/test_position_restraint.py +++ b/tests/Sandpit/Exscientia/Process/test_position_restraint.py @@ -236,8 +236,8 @@ def test_gromacs_alchemical_ion( @pytest.mark.skipif( - has_amber is False or has_openff is False, - reason="Requires AMBER and openff to be installed", + has_amber is False or has_gromacs is False or has_openff is False, + reason="Requires AMBER, GROMACS and OpenFF to be installed", ) @pytest.mark.parametrize( ("restraint", "target"), From b7f68bacc56e6f3e1c1edb64126ea081295a4798 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 15 Apr 2024 13:14:49 +0100 Subject: [PATCH 119/121] Update CHANGELONG for the 2024.1.0 release. --- doc/source/changelog.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 0e8765d6c..ad0eba596 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -9,6 +9,20 @@ company supporting open-source development of fostering academic/industrial coll within the biomolecular simulation community. Our software is hosted via the `OpenBioSim` `GitHub `__ organisation. +`2024.1.0 `_ - Ap4 15 2024 +------------------------------------------------------------------------------------------------- + +* Switch to using Langevin integrator for GROMACS free energy simulations (`#264 `__). +* Add support for clearing and disabling the IO file cache (`#266 `__). +* Add support for using ``openff-nagl`` to generate partial charges (`#267 `__). +* Fixed non-reproducible search for backbone restraint atom indices (`#270 `__). +* Add support for AMBER as an alchemical free-energy simulation engine (`#272 `__). +* Switch to using ``os.path.join`` to generate directory file names (`#276 `__). +* Make sure the ``fileformat`` property is preserved when creating single molecule systems (`#276 `__). +* Add a ``getRestraintType`` method to the base protocol that returns ``None`` (`#276 `__). +* Add missing ``themostat_time_constant`` kwarg to the :class:`Metadynamics ` protocol (`#276 `__). + + `2023.5.1 `_ - Mar 20 2024 ------------------------------------------------------------------------------------------------- From 769ff1931c5315ef3d2cccde563e0ffa52c4891f Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 15 Apr 2024 13:18:09 +0100 Subject: [PATCH 120/121] Update Sire version. --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a5caf0018..0bc33ac3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ # BioSimSpace runtime requirements. # main -#sire~=2023.5.2 +sire~=2024.1.0 # devel -sire==2024.1.0.dev +#sire==2024.1.0.dev configargparse ipywidgets From 589cecce8a5ca3244702dd154d635df4bea2e1cb Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 15 Apr 2024 14:17:14 +0100 Subject: [PATCH 121/121] Typo. [ci skip] --- doc/source/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index ad0eba596..44d79ec3b 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -9,7 +9,7 @@ company supporting open-source development of fostering academic/industrial coll within the biomolecular simulation community. Our software is hosted via the `OpenBioSim` `GitHub `__ organisation. -`2024.1.0 `_ - Ap4 15 2024 +`2024.1.0 `_ - Apr 15 2024 ------------------------------------------------------------------------------------------------- * Switch to using Langevin integrator for GROMACS free energy simulations (`#264 `__).