diff --git a/.github/workflows/gh-ci.yaml b/.github/workflows/gh-ci.yaml index f501d9a..0890447 100644 --- a/.github/workflows/gh-ci.yaml +++ b/.github/workflows/gh-ci.yaml @@ -103,7 +103,6 @@ jobs: conda info conda list - - name: Run IMDClient tests run: | pytest -n auto -v --cov=imdclient --cov-report=xml --color=yes imdclient/tests/test_imdclient.py @@ -120,6 +119,69 @@ jobs: name: codecov-${{ matrix.os }}-py${{ matrix.python-version }} verbose: True + simulation_engine_tests: + if: github.repository == 'Becksteinlab/imdclient' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build information + run: | + uname -a + df -h + ulimit -a + + - name: Install conda dependencies + uses: conda-incubator/setup-miniconda@v2 + with: + python-version: "3.11" + environment-file: devtools/conda-envs/test_env.yaml + add-pip-as-python-dependency: true + architecture: x64 + + channels: conda-forge, defaults + + activate-environment: imdclient-test + auto-update-conda: true + auto-activate-base: false + show-channel-urls: true + miniconda-version: latest + + - name: Install MDAnalysis version + uses: MDAnalysis/install-mdanalysis@main + with: + version: latest + install-tests: true + installer: conda + shell: bash -l {0} + + - name: Install package + run: | + python --version + python -m pip install . --no-deps + + - name: Pull container + run: | + docker pull ghcr.io/becksteinlab/streaming-md-docker:main-Common-CPU + + - name: Run GROMACS tests + run: | + pytest -v --color=yes imdclient/tests/test_gromacs.py + + - name: Run LAMMPS tests + run: | + pytest -v --color=yes imdclient/tests/test_lammps.py + + - name: Run NAMD tests + run: | + pytest -v --color=yes imdclient/tests/test_namd.py + + - name: Dump docker logs on failure + if: failure() + run: | + docker logs sim + pylint_check: if: github.repository == 'Becksteinlab/imdclient' needs: environment-config diff --git a/.github/workflows/gromacs-integration.yaml b/.github/workflows/gromacs-integration.yaml deleted file mode 100644 index d7edf23..0000000 --- a/.github/workflows/gromacs-integration.yaml +++ /dev/null @@ -1,87 +0,0 @@ -name: GH Actions CI -on: - push: - branches: - - main - pull_request: - branches: - - main - - develop - -defaults: - run: - shell: bash -l {0} - - - -jobs: - gromacs-tests: - if: github.repository == 'Becksteinlab/imdclient' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Build information - run: | - uname -a - df -h - ulimit -a - - # More info on options: https://github.com/conda-incubator/setup-miniconda - - name: Install conda dependencies - uses: conda-incubator/setup-miniconda@v2 - with: - python-version: "3.12" - environment-file: devtools/conda-envs/test_env.yaml - add-pip-as-python-dependency: true - - channels: conda-forge, defaults - - activate-environment: imdreader-integration-test - auto-update-conda: true - auto-activate-base: false - show-channel-urls: true - miniconda-version: latest - - - - name: Install MDAnalysis version - uses: MDAnalysis/install-mdanalysis@main - with: - version: "2.7.0" - install-tests: true - installer: conda - shell: bash -l {0} - - - name: Install package - run: | - python --version - python -m pip install . --no-deps - - - name: Python information - run: | - which python - which pip - pip list - conda info - conda list - - - name: Clone gromacs fork - uses: actions/checkout@v4 - with: - repository: hcho38/gromacs - ref: imd-v3 - path: gromacs - - - name: Build gromacs - run: | - cd gromacs - mkdir build - cd build - cmake .. -DGMX_BUILD_OWN_FFTW=ON -DREGRESSIONTEST_DOWNLOAD=ON - make - sudo make install - source /usr/local/gromacs/bin/GMXRC - - - name: Run gromacs tests - run: | - pytest -n auto -v --color=yes imdclient/tests/test_gromacs.py diff --git a/.github/workflows/lammps-integration.yaml b/.github/workflows/lammps-integration.yaml deleted file mode 100644 index 90fe9a1..0000000 --- a/.github/workflows/lammps-integration.yaml +++ /dev/null @@ -1,85 +0,0 @@ -name: GH Actions CI -on: - push: - branches: - - main - pull_request: - branches: - - main - - develop - -defaults: - run: - shell: bash -l {0} - - -jobs: - lammps-tests: - if: github.repository == 'Becksteinlab/imdclient' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Build information - run: | - uname -a - df -h - ulimit -a - - # More info on options: https://github.com/conda-incubator/setup-miniconda - - name: Install conda dependencies - uses: conda-incubator/setup-miniconda@v2 - with: - python-version: "3.12" - environment-file: devtools/conda-envs/test_env.yaml - add-pip-as-python-dependency: true - - channels: conda-forge, defaults - - activate-environment: imdreader-integration-test - auto-update-conda: true - auto-activate-base: false - show-channel-urls: true - miniconda-version: latest - - - name: Install MDAnalysis version - uses: MDAnalysis/install-mdanalysis@main - with: - version: "2.7.0" - install-tests: true - installer: conda - shell: bash -l {0} - - - name: Install package - run: | - python --version - python -m pip install . --no-deps - - - name: Python information - run: | - which python - which pip - pip list - conda info - conda list - - - name: Clone lammps fork - uses: actions/checkout@v4 - with: - repository: ljwoods2/lammps - ref: imd-v3 - path: lammps - - - name: Build lammps - run: | - cd lammps - mkdir build - cd build - cmake ../cmake/ -D PKG_MISC=yes - cmake --build . - make install - lmp -h - - - name: Run lammps tests - run: | - pytest -n auto -v --color=yes imdclient/tests/test_lammps.py diff --git a/.github/workflows/namd-integration.yaml b/.github/workflows/namd-integration.yaml deleted file mode 100644 index e1545bd..0000000 --- a/.github/workflows/namd-integration.yaml +++ /dev/null @@ -1,101 +0,0 @@ -name: GH Actions CI -on: - push: - branches: - - main - pull_request: - branches: - - main - - develop - -defaults: - run: - shell: bash -l {0} - - - -jobs: - namd-tests: - if: github.repository == 'Becksteinlab/imdclient' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Build information - run: | - uname -a - df -h - ulimit -a - - # More info on options: https://github.com/conda-incubator/setup-miniconda - - name: Install conda dependencies - uses: conda-incubator/setup-miniconda@v2 - with: - python-version: "3.12" - environment-file: devtools/conda-envs/test_env.yaml - add-pip-as-python-dependency: true - - channels: conda-forge, defaults - - activate-environment: imdreader-integration-test - auto-update-conda: true - auto-activate-base: false - show-channel-urls: true - miniconda-version: latest - - - - name: Install MDAnalysis version - uses: MDAnalysis/install-mdanalysis@main - with: - version: "2.7.0" - install-tests: true - installer: conda - shell: bash -l {0} - - - name: Install package - run: | - python --version - python -m pip install . --no-deps - - - name: Python information - run: | - which python - which pip - pip list - conda info - conda list - - - name: Clone namd fork - uses: actions/checkout@v4 - with: - repository: amruthesht/namd-3.0 - ref: IMDv3-dev - path: namd - - - name: Build namd - run: | - cd namd - tar xf charm-8.0.0.tar - cd charm-8.0.0 - ./build charm++ multicore-linux-x86_64 --with-production - cd multicore-linux-x86_64/tests/charm++/megatest - make - cd ../../../../.. - wget https://www.fftw.org/fftw-2.1.5.tar.gz - tar xzf fftw-2.1.5.tar.gz - cd fftw-2.1.5 - ./configure --enable-float --enable-type-prefix --enable-static - make - sudo make install - cd .. - wget http://www.ks.uiuc.edu/Research/namd/libraries/tcl8.6.13-linux-x86_64-threaded.tar.gz - tar xzf tcl8.6.13-linux-x86_64-threaded.tar.gz - mv tcl8.6.13-linux-x86_64-threaded tcl-threaded - ./config Linux-x86_64-g++.simple --charm-arch multicore-linux-x86_64 --fftw-prefix "/usr/local" - cd Linux-x86_64-g++.simple - make - sudo ln -s $PWD/namd3 /usr/local/bin/namd3 - - - name: Run namd tests - run: | - pytest -n auto -v --color=yes imdclient/tests/test_namd.py diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index c980b16..8a5e11e 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -16,6 +16,7 @@ dependencies: - pytest-xdist - codecov - MDAnalysisTests>=2.7.0 + - docker-py # Pip-only installs #- pip: diff --git a/imdclient/data/gromacs/md/gromacs_v3_nst1.tpr b/imdclient/data/gromacs/md/gromacs_v3_nst1.tpr deleted file mode 100644 index 62c7ea8..0000000 Binary files a/imdclient/data/gromacs/md/gromacs_v3_nst1.tpr and /dev/null differ diff --git a/imdclient/data/gromacs/md/gromacs_v3_nst1.trr b/imdclient/data/gromacs/md/gromacs_v3_nst1.trr deleted file mode 100644 index 9b9a2b8..0000000 Binary files a/imdclient/data/gromacs/md/gromacs_v3_nst1.trr and /dev/null differ diff --git a/imdclient/data/gromacs/md/gromacs_v3_nst8.mdp b/imdclient/data/gromacs/md/gromacs_v3_nst8.mdp new file mode 100644 index 0000000..96e85a6 --- /dev/null +++ b/imdclient/data/gromacs/md/gromacs_v3_nst8.mdp @@ -0,0 +1,58 @@ +title = PRODUCTION IN NPT +ld-seed = 1 +; Run parameters +integrator = md ; leap-frog integrator +nsteps = 100 ; 1 * 1000 = 1 ps +dt = 0.001 ; 1 fs +; Output control +nstxout = 8 ; save coordinates every 1 fs +nstvout = 8 ; save velocities every 1 fs +nstfout = 8 +nstenergy = 8 ; save energies every 1 fs +nstlog = 10 ; update log file every 1 ps +; Center of mass (COM) motion +nstcomm = 10 ; remove COM motion every 10 steps +comm-mode = Linear ; remove only COM translation (liquids in PBC) +; Bond parameters +continuation = yes ; first dynamics run +constraint_algorithm = lincs ; holonomic constraints +constraints = all-bonds ; all bonds lengths are constrained +lincs_iter = 1 ; accuracy of LINCS +lincs_order = 4 ; also related to accuracy +; Nonbonded settings +cutoff-scheme = Verlet ; Buffered neighbor searching +ns_type = grid ; search neighboring grid cells +nstlist = 10 ; 10 fs, largely irrelevant with Verlet +rcoulomb = 1.0 ; short-range electrostatic cutoff (in nm) +rvdw = 1.0 ; short-range van der Waals cutoff (in nm) +DispCorr = EnerPres ; account for cut-off vdW scheme +; Electrostatics +coulombtype = PME ; Particle Mesh Ewald for long-range electrostatics +pme_order = 4 ; cubic interpolation +fourierspacing = 0.12 ; grid spacing for FFT +; Temperature coupling is on +tcoupl = Nose-Hoover ; good for production, after equilibration +; we define separate thermostats for the solute and solvent (need to adapt) +; see default groups defined by Gromacs for your system or define your own (make_ndx) +tc-grps = Protein SOL ; the separate groups for the thermostats +tau-t = 1.0 1.0 ; time constants for thermostats (ps) +ref-t = 300 300 ; reference temperature for thermostats (K) +; Pressure coupling is off +pcoupl = Parrinello-Rahman ; good for production, after equilibration +tau-p = 2.0 ; time constant for barostat (ps) +compressibility = 4.5e-5 ; compressibility (1/bar) set to water at ~300K +ref-p = 1.0 ; reference pressure for barostat (bar) +; Periodic boundary conditions +pbc = xyz ; 3-D PBC +; Velocity generation +gen_vel = no +IMD-group = System +IMD-nst = 8 +IMD-version = 3 +IMD-time = yes +IMD-box = yes +IMD-coords = yes +IMD-unwrap = no +IMD-vels = yes +IMD-forces = yes +IMD-energies = no diff --git a/imdclient/data/lammps/md/lammps_trj.h5md b/imdclient/data/lammps/md/lammps_trj.h5md deleted file mode 100644 index 51f5c28..0000000 Binary files a/imdclient/data/lammps/md/lammps_trj.h5md and /dev/null differ diff --git a/imdclient/data/lammps/md/lammps_v3.in b/imdclient/data/lammps/md/lammps_v3_nst_1.in similarity index 95% rename from imdclient/data/lammps/md/lammps_v3.in rename to imdclient/data/lammps/md/lammps_v3_nst_1.in index 8dcff3f..f848785 100644 --- a/imdclient/data/lammps/md/lammps_v3.in +++ b/imdclient/data/lammps/md/lammps_v3_nst_1.in @@ -52,8 +52,8 @@ velocity all create 300 102939 dist gaussian mom yes rot yes fix 1 all nve # Create source of truth trajectory -# dump h5md1 all h5md 1 lammps_trj.h5md position velocity force box yes -# dump_modify h5md1 unwrap no +dump h5md1 all h5md 1 lammps_trj.h5md position velocity force box yes +dump_modify h5md1 unwrap no ## IMD settings # https://docs.lammps.org/fix_imd.html @@ -63,7 +63,7 @@ fix 2 all imd 8888 version 3 unwrap off nowait off run 100 # Stop dumping information to the dump file. -# undump h5md1 +undump h5md1 # Unfix the NVE. Additional lines if any will assume that this fix is off. unfix 1 diff --git a/imdclient/data/lammps/md/lammps_v3_nst_8.in b/imdclient/data/lammps/md/lammps_v3_nst_8.in new file mode 100644 index 0000000..8336fb9 --- /dev/null +++ b/imdclient/data/lammps/md/lammps_v3_nst_8.in @@ -0,0 +1,71 @@ +## Setup +units metal +boundary p p p #Specify periodic boundary condition are needed in all three faces +atom_style atomic #What style of atoms is to be used in the simulation +log logfile.txt #Write the log file to this text file. All thermodynamic information applicable to the entire system + +## Create Box +#Refers to an abstract geometric region of space. units box refers to the fact that the size of the box is specified in the units as given in the units command. +# The name "forbox" refers to the region ID so that you can refer to it somewhere else in this input script. +region forbox block 0 45.8 0 45.8 0 45.8 units box +create_box 1 forbox +# Since we have given fcc as lattice type no need to mention basis for this +lattice fcc 4.58 + +## Create atoms & define interactions +# basis arg defines which atoms are created based on their lattice position (all are atom type 1) +create_atoms 1 region forbox basis 1 1 basis 2 1 basis 3 1 basis 4 1 units box +# Mass of atom type 1 is 39.48 [mass units grams/mole] +mass 1 39.948 +# lj potential describes potential energy between two atoms as function of the dist between them +# don't apply lj interactions beyond cutoff dist +pair_style lj/cut 10 +# The coefficient of the lj potential for the interactions of atom type 1 with atom type 1 +pair_coeff 1 1 0.01006418 3.3952 + +## Create atom group for argon atoms +group ar type 1 #Group all the argon types (argon type is of type 1). All atoms of type 1 are in group with the name 'ar' + + +## Write initial configuration +dump dump_1 all custom 1 dump_initial_config.dump id type x y z ix iy iz vx vy vz + + +## Perform energy minimization +run 1 +# Stop dumping to this file +undump dump_1 +# Minimize the energy using a conjugate gradient step. +minimize 1e-25 1e-19 10000 10000 +print "Finished Minimizing" +variable ener equal pe + +## Output the topology after minimization +write_data topology_after_min.data + +## Prepare MD simulation +timestep 0.001 +# Set the velocities of all the atoms so that the temperature of the system +# is 300K. Make the distribution Gaussian. +velocity all create 300 102939 dist gaussian mom yes rot yes +# this is equlibration process. +fix 1 all nve + +# Create source of truth trajectory +dump h5md1 all h5md 8 lammps_trj.h5md position velocity force box yes +dump_modify h5md1 unwrap no + +## IMD settings +# https://docs.lammps.org/fix_imd.html +fix 2 all imd 8888 version 3 unwrap off nowait off trate 8 + +## Run MD sim +run 100 + +# Stop dumping information to the dump file. +undump h5md1 + +# Unfix the NVE. Additional lines if any will assume that this fix is off. +unfix 1 + +#End diff --git a/imdclient/data/namd/md/alanin.dcd b/imdclient/data/namd/md/alanin.dcd deleted file mode 100644 index 3947576..0000000 Binary files a/imdclient/data/namd/md/alanin.dcd and /dev/null differ diff --git a/imdclient/data/namd/md/namd_v3.namd b/imdclient/data/namd/md/namd_v3_nst_1.namd similarity index 62% rename from imdclient/data/namd/md/namd_v3.namd rename to imdclient/data/namd/md/namd_v3_nst_1.namd index 3007642..4ae586f 100644 --- a/imdclient/data/namd/md/namd_v3.namd +++ b/imdclient/data/namd/md/namd_v3_nst_1.namd @@ -17,8 +17,20 @@ switchdist 7.0 cutoff 8.0 pairlistdist 9.0 -dcdfile alanin.dcd -dcdfreq 1 +# Add box dimensions +cellBasisVector1 32.76 0.0 0.0 +cellBasisVector2 0.0 31.66 0.0 +cellBasisVector3 0.0 0.0 32.89 + +DCDfile alanin.dcd +DCDfreq 1 +DCDUnitCell yes +velDcdFile alanin.vel.dcd +velDcdFreq 1 +forceDcdFile alanin.force.dcd +forceDcdFreq 1 +XSTFile alanin.xst +xstFreq 1 #restartname alanin.restart #restartfreq 10 @@ -40,8 +52,8 @@ IMDwait on IMDversion 3 IMDsendPositions yes IMDsendEnergies yes -#IMDsendTime yes -#IMDsendBoxDimensions yes +IMDsendTime yes +IMDsendBoxDimensions yes IMDsendVelocities yes -#IMDsendForces yes +IMDsendForces yes IMDwrapPositions yes \ No newline at end of file diff --git a/imdclient/data/namd/md/namd_v3_nst_8.namd b/imdclient/data/namd/md/namd_v3_nst_8.namd new file mode 100644 index 0000000..80456ca --- /dev/null +++ b/imdclient/data/namd/md/namd_v3_nst_8.namd @@ -0,0 +1,59 @@ +# This is a test namd configuration file + +timestep 0.5 +numsteps 100 +structure alanin.psf +parameters alanin.params +coordinates alanin.pdb +exclude scaled1-4 +1-4scaling 0.4 +outputname output[myReplica] +margin 1.0 +stepspercycle 3 +temperature 0 + +switching on +switchdist 7.0 +cutoff 8.0 +pairlistdist 9.0 + +# Add box dimensions +cellBasisVector1 32.76 0.0 0.0 +cellBasisVector2 0.0 31.66 0.0 +cellBasisVector3 0.0 0.0 32.89 + +DCDfile alanin.dcd +DCDfreq 8 +DCDUnitCell yes +velDcdFile alanin.vel.dcd +velDcdFreq 8 +forceDcdFile alanin.force.dcd +forceDcdFreq 8 +XSTFile alanin.xst +xstFreq 8 + +#restartname alanin.restart +#restartfreq 10 + +#langevin on +#langevinTemp 300.0 +#langevincol O + +#constraints on + +#fma on + +seed 12345 + +IMDon yes +IMDport 8888 +IMDfreq 8 +IMDwait on +IMDversion 3 +IMDsendPositions yes +IMDsendEnergies yes +IMDsendTime yes +IMDsendBoxDimensions yes +IMDsendVelocities yes +IMDsendForces yes +IMDwrapPositions yes \ No newline at end of file diff --git a/imdclient/tests/base.py b/imdclient/tests/base.py index 597d044..2eeb189 100644 --- a/imdclient/tests/base.py +++ b/imdclient/tests/base.py @@ -1,4 +1,5 @@ from imdclient.IMDClient import IMDClient +from imdclient.IMDREADER import IMDReader import pytest from pathlib import Path import os @@ -9,8 +10,11 @@ assert_allclose, ) import numpy as np - +import docker import logging +import shutil +import MDAnalysis as mda +from .utils import get_free_port logger = logging.getLogger("imdclient.IMDClient") @@ -61,62 +65,146 @@ def assert_allclose_with_logging(a, b, rtol=1e-07, atol=0, equal_nan=False): class IMDv3IntegrationTest: @pytest.fixture() - def run_sim_and_wait(self, tmp_path, command, match_string): - old_cwd = Path.cwd() - os.chdir(tmp_path) - p = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - shell=True, - text=True, - bufsize=0, - preexec_fn=os.setsid, + def setup_command(self): + return None + + @pytest.fixture() + def port(self): + yield get_free_port() + + @pytest.fixture() + def docker_client( + self, + tmp_path, + input_files, + setup_command, + simulation_command, + port, + ): + # In CI, container process needs access to tmp_path + tmp_path.chmod(0o777) + docker_client = docker.from_env() + img = docker_client.images.pull( + "ghcr.io/becksteinlab/streaming-md-docker:main-Common-CPU" + ) + # Copy input files into tmp_path + for inp in input_files: + shutil.copy(inp, tmp_path) + + cmdstring = "cd '/tmp'" + # Run the setup command, if any + if setup_command is not None: + # This should be blocking + cmdstring += " && " + setup_command + + cmdstring += " && " + simulation_command + + # Start the container, mount tmp_path, run simulation + container = docker_client.containers.run( + img, + f"/bin/sh -c '{cmdstring}'", + detach=True, + volumes={tmp_path.as_posix(): {"bind": "/tmp", "mode": "rw"}}, + ports={"8888/tcp": port}, + name="sim", + remove=True, ) - t = time.time() - for stdout_line in iter(p.stdout.readline, ""): - logger.debug(f"stdout: {stdout_line}") - if match_string in stdout_line: + # For now, just wait 30 seconds + # life is too short to figure out how to redirect all stdout from inside + # a container + time.sleep(30) + + yield + try: + container.stop() + except docker.errors.NotFound: + pass + + @pytest.fixture() + def imd_u(self, docker_client, topol, tmp_path, port): + u = mda.Universe((tmp_path / topol), f"imd://localhost:{port}") + with mda.Writer( + (tmp_path / "imd.trr").as_posix(), u.trajectory.n_atoms + ) as w: + for ts in u.trajectory: + w.write(u.atoms) + yield mda.Universe((tmp_path / topol), (tmp_path / "imd.trr")) + + @pytest.fixture() + def true_u(self, topol, traj, imd_u, tmp_path): + u = mda.Universe( + (tmp_path / topol), + (tmp_path / traj), + ) + yield u - break - if time.time() - t > 10: - raise TimeoutError("Timeout waiting for match string") + @pytest.fixture() + def comp_time(self): + return True - logger.debug("Match string found") - yield p - os.chdir(old_cwd) - os.killpg(os.getpgid(p.pid), signal.SIGTERM) + @pytest.fixture() + def comp_dt(self): + return True @pytest.fixture() - def client(self, run_sim_and_wait, universe): - client = IMDClient("localhost", 8888, universe.trajectory.n_atoms) - yield client - client.stop() - - def test_compare_imd_to_true_traj(self, universe, client, first_frame): - imdsinfo = client.get_imdsessioninfo() - - for ts in universe.trajectory[first_frame:]: - imdf = client.get_imdframe() - if imdsinfo.time: - assert_allclose(imdf.time, ts.time, atol=1e-03) - assert_allclose(imdf.step, ts.data["step"]) - if imdsinfo.box: + def comp_step(self): + return True + + def test_compare_imd_to_true_traj( + self, imd_u, true_u, first_frame, comp_time, comp_dt, comp_step + ): + for i in range(first_frame, len(true_u.trajectory)): + if comp_time: + assert_allclose( + true_u.trajectory[i].time, + imd_u.trajectory[i - first_frame].time, + atol=1e-03, + ) + if comp_dt: + assert_allclose( + true_u.trajectory[i].dt, + imd_u.trajectory[i - first_frame].dt, + atol=1e-03, + ) + if comp_step: + assert_allclose( + true_u.trajectory[i].data["step"], + imd_u.trajectory[i - first_frame].data["step"], + ) + if ( + true_u.trajectory[i].dimensions is not None + and imd_u.trajectory[i - first_frame].dimensions is not None + ): assert_allclose_with_logging( - imdf.box, - ts.triclinic_dimensions, + true_u.trajectory[i].dimensions, + imd_u.trajectory[i - first_frame].dimensions, atol=1e-03, ) - if imdsinfo.positions: + if ( + true_u.trajectory[i].has_positions + and imd_u.trajectory[i - first_frame].has_positions + ): assert_allclose_with_logging( - imdf.positions, ts.positions, atol=1e-03 + true_u.trajectory[i].positions, + imd_u.trajectory[i - first_frame].positions, + atol=1e-03, ) - if imdsinfo.velocities: + if ( + true_u.trajectory[i].has_velocities + and imd_u.trajectory[i - first_frame].has_velocities + ): assert_allclose_with_logging( - imdf.velocities, ts.velocities, atol=1e-03 + true_u.trajectory[i].velocities, + imd_u.trajectory[i - first_frame].velocities, + atol=1e-03, ) - if imdsinfo.forces: + if ( + true_u.trajectory[i].has_forces + and imd_u.trajectory[i - first_frame].has_forces + ): assert_allclose_with_logging( - imdf.forces, ts.forces, atol=1e-03 + true_u.trajectory[i].forces, + imd_u.trajectory[i - first_frame].forces, + atol=1e-03, ) diff --git a/imdclient/tests/datafiles.py b/imdclient/tests/datafiles.py index 5e543a7..282749e 100644 --- a/imdclient/tests/datafiles.py +++ b/imdclient/tests/datafiles.py @@ -16,18 +16,26 @@ _data_ref = resources.files("imdclient.data") LAMMPS_TOPOL = (_data_ref / "lammps" / "md" / "lammps_topol.data").as_posix() -LAMMPS_IN = (_data_ref / "lammps" / "md" / "lammps_v3.in").as_posix() -LAMMPS_TRAJ = (_data_ref / "lammps" / "md" / "lammps_trj.h5md").as_posix() -GROMACS_TRAJ = ( - _data_ref / "gromacs" / "md" / "gromacs_v3_nst1.trr" +LAMMPS_IN_NST_1 = ( + _data_ref / "lammps" / "md" / "lammps_v3_nst_1.in" ).as_posix() -GROMACS_TOPOL = ( - _data_ref / "gromacs" / "md" / "gromacs_struct.gro" +LAMMPS_IN_NST_8 = ( + _data_ref / "lammps" / "md" / "lammps_v3_nst_8.in" ).as_posix() -GROMACS_TPR = (_data_ref / "gromacs" / "md" / "gromacs_v3_nst1.tpr").as_posix() + + +GROMACS_GRO = (_data_ref / "gromacs" / "md" / "gromacs_struct.gro").as_posix() +GROMACS_MDP_NST_1 = ( + _data_ref / "gromacs" / "md" / "gromacs_v3_nst1.mdp" +).as_posix() +GROMACS_MDP_NST_8 = ( + _data_ref / "gromacs" / "md" / "gromacs_v3_nst8.mdp" +).as_posix() +GROMACS_TOP = (_data_ref / "gromacs" / "md" / "gromacs_v3.top").as_posix() + NAMD_TOPOL = (_data_ref / "namd" / "md" / "alanin.pdb").as_posix() -NAMD_CONF = (_data_ref / "namd" / "md" / "namd_v3.namd").as_posix() -NAMD_TRAJ = (_data_ref / "namd" / "md" / "alanin.dcd").as_posix() +NAMD_CONF_NST_1 = (_data_ref / "namd" / "md" / "namd_v3_nst_1.namd").as_posix() +NAMD_CONF_NST_8 = (_data_ref / "namd" / "md" / "namd_v3_nst_8.namd").as_posix() NAMD_PARAMS = (_data_ref / "namd" / "md" / "alanin.params").as_posix() NAMD_PSF = (_data_ref / "namd" / "md" / "alanin.psf").as_posix() diff --git a/imdclient/tests/docker_testing/docker.md b/imdclient/tests/docker_testing/docker.md new file mode 100644 index 0000000..32dade9 --- /dev/null +++ b/imdclient/tests/docker_testing/docker.md @@ -0,0 +1,25 @@ +# Running simulation engines compiled for GPU acceleration from a container + +Ensure [docker](https://www.docker.com/) and the [NVIDIA container toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) are installed. + + +To run the container: +```bash +docker pull ghcr.io/becksteinlab/streaming-md-docker:main-Common-GPU + +docker run -v $PWD/imdclient/data:/home/conda:rw -it --runtime=nvidia --gpus=all \ + ghcr.io/becksteinlab/streaming-md-docker:main-Common-GPU +``` + +To run each simulation engine with repository simulation configurations: +```bash +cd /home/conda/namd/md +namd3 +devices 0 namd_v3.namd + +cd /home/conda/gromacs/md +gmx grompp -f gromacs_v3_nst1.mdp -c gromacs_struct.gro -p gromacs_v3.top +gmx mdrun -s topol.tpr -nb gpu + +cd /home/conda/lammps/md +lmp -sf gpu < lammps_v3_nst_1.in +``` \ No newline at end of file diff --git a/imdclient/tests/test_gromacs.py b/imdclient/tests/test_gromacs.py index 46606d9..d4fad72 100644 --- a/imdclient/tests/test_gromacs.py +++ b/imdclient/tests/test_gromacs.py @@ -2,10 +2,16 @@ import pytest import logging from .base import IMDv3IntegrationTest -from .datafiles import GROMACS_TOPOL, GROMACS_TRAJ, GROMACS_TPR +from .datafiles import ( + GROMACS_GRO, + GROMACS_TOP, + GROMACS_MDP_NST_1, + GROMACS_MDP_NST_8, +) +from pathlib import Path logger = logging.getLogger("imdclient.IMDClient") -file_handler = logging.FileHandler("test.log") +file_handler = logging.FileHandler("gromacs_test.log") formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) @@ -16,18 +22,34 @@ class TestIMDv3Gromacs(IMDv3IntegrationTest): + @pytest.fixture(params=[GROMACS_MDP_NST_1, GROMACS_MDP_NST_8]) + def mdp(self, request): + return request.param + @pytest.fixture() - def command(self): - return f". /usr/local/gromacs/bin/GMXRC && gmx mdrun -s {GROMACS_TPR} -imdport 8888 -imdwait" + def setup_command(self, mdp): + return f"gmx grompp -f {Path(mdp).name} -c {Path(GROMACS_GRO).name} -p {Path(GROMACS_TOP).name} -o topol.tpr" @pytest.fixture() - def match_string(self): - return "IMD: Will wait until I have a connection and IMD_GO orders." + def simulation_command(self): + return f"gmx mdrun -s topol.tpr -o ci.trr -imdport 8888 -imdwait" @pytest.fixture() - def first_frame(self): - return 0 + def input_files(self, mdp): + return [GROMACS_TOP, mdp, GROMACS_GRO] + + @pytest.fixture() + def traj(self): + return "ci.trr" @pytest.fixture() - def universe(self): - return mda.Universe(GROMACS_TOPOL, GROMACS_TRAJ) + def topol(self): + return Path(GROMACS_GRO).name + + # @pytest.fixture() + # def match_string(self): + # return "IMD: Will wait until I have a connection and IMD_GO orders." + + @pytest.fixture() + def first_frame(self): + return 0 diff --git a/imdclient/tests/test_lammps.py b/imdclient/tests/test_lammps.py index 118119b..31f1af8 100644 --- a/imdclient/tests/test_lammps.py +++ b/imdclient/tests/test_lammps.py @@ -1,11 +1,13 @@ +from imdclient.IMDREADER import IMDReader import MDAnalysis as mda import pytest +from pathlib import Path import logging from .base import IMDv3IntegrationTest -from .datafiles import LAMMPS_IN, LAMMPS_TOPOL, LAMMPS_TRAJ +from .datafiles import LAMMPS_TOPOL, LAMMPS_IN_NST_1, LAMMPS_IN_NST_8 logger = logging.getLogger("imdclient.IMDClient") -file_handler = logging.FileHandler("test.log") +file_handler = logging.FileHandler("lammps_test.log") formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) @@ -16,23 +18,66 @@ class TestIMDv3Lammps(IMDv3IntegrationTest): + @pytest.fixture(params=[LAMMPS_IN_NST_1, LAMMPS_IN_NST_8]) + def inp(self, request): + return request.param + + @pytest.fixture() + def simulation_command(self, inp): + return f"lmp < {Path(inp).name}" + + @pytest.fixture() + def topol(self): + return Path(LAMMPS_TOPOL).name + + @pytest.fixture() + def traj(self): + return "lammps_trj.h5md" + @pytest.fixture() - def command(self): - return f"lmp < {LAMMPS_IN}" + def input_files(self, inp): + return [inp, LAMMPS_TOPOL] + + # @pytest.fixture() + # def match_string(self): + # return "Waiting for IMD connection on port 8888" @pytest.fixture() - def match_string(self): - return "Waiting for IMD connection on port 8888" + def first_frame(self, inp): + if inp == LAMMPS_IN_NST_1: + return 1 + else: + return 0 @pytest.fixture() - def first_frame(self): - return 1 + def comp_dt(self): + return False + # This must wait until after imd stream has ended @pytest.fixture() - def universe(self): - return mda.Universe( - LAMMPS_TOPOL, - LAMMPS_TRAJ, + def true_u(self, topol, traj, imd_u, tmp_path): + u = mda.Universe( + (tmp_path / topol), + (tmp_path / traj), atom_style="id type x y z", convert_units=False, ) + yield u + + @pytest.fixture() + def imd_u(self, docker_client, topol, tmp_path, port): + u = mda.Universe( + (tmp_path / topol), + f"imd://localhost:{port}", + atom_style="id type x y z", + ) + with mda.Writer( + (tmp_path / "imd.trr").as_posix(), u.trajectory.n_atoms + ) as w: + for ts in u.trajectory: + w.write(u.atoms) + yield mda.Universe( + (tmp_path / topol), + (tmp_path / "imd.trr"), + atom_style="id type x y z", + ) diff --git a/imdclient/tests/test_manual.py b/imdclient/tests/test_manual.py index cde3c0d..4fddaa9 100644 --- a/imdclient/tests/test_manual.py +++ b/imdclient/tests/test_manual.py @@ -59,33 +59,35 @@ def test_compare_imd_to_true_traj(self, true_u, imd_u, first_frame_arg): for i in range(first_frame_arg, len(true_u.trajectory)): assert_allclose( - true_u.trajectory[i].time, imd_u.trajectory[i].time, atol=1e-03 + true_u.trajectory[i].time, + imd_u.trajectory[i - first_frame_arg].time, + atol=1e-03, ) assert_allclose( true_u.trajectory[i].data["step"], - imd_u.trajectory[i].data["step"], + imd_u.trajectory[i - first_frame_arg].data["step"], ) if true_u.trajectory[i].dimensions is not None: assert_allclose_with_logging( true_u.trajectory[i].dimensions, - imd_u.trajectory[i].dimensions, + imd_u.trajectory[i - first_frame_arg].dimensions, atol=1e-03, ) if true_u.trajectory[i].has_positions: assert_allclose_with_logging( true_u.trajectory[i].positions, - imd_u.trajectory[i].positions, + imd_u.trajectory[i - first_frame_arg].positions, atol=1e-03, ) if true_u.trajectory[i].has_velocities: assert_allclose_with_logging( true_u.trajectory[i].velocities, - imd_u.trajectory[i].velocities, + imd_u.trajectory[i - first_frame_arg].velocities, atol=1e-03, ) if true_u.trajectory[i].has_forces: assert_allclose_with_logging( true_u.trajectory[i].forces, - imd_u.trajectory[i].forces, + imd_u.trajectory[i - first_frame_arg].forces, atol=1e-03, ) diff --git a/imdclient/tests/test_namd.py b/imdclient/tests/test_namd.py index 5eea88e..f1312b2 100644 --- a/imdclient/tests/test_namd.py +++ b/imdclient/tests/test_namd.py @@ -1,11 +1,21 @@ import MDAnalysis as mda import pytest import logging -from .base import IMDv3IntegrationTest -from .datafiles import NAMD_TOPOL, NAMD_CONF, NAMD_TRAJ, NAMD_PARAMS, NAMD_PSF +from .base import IMDv3IntegrationTest, assert_allclose_with_logging +from .datafiles import ( + NAMD_TOPOL, + NAMD_CONF_NST_1, + NAMD_CONF_NST_8, + NAMD_PARAMS, + NAMD_PSF, +) +from pathlib import Path +from numpy.testing import ( + assert_allclose, +) logger = logging.getLogger("imdclient.IMDClient") -file_handler = logging.FileHandler("test.log") +file_handler = logging.FileHandler("namd_test.log") formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) @@ -16,23 +26,100 @@ class TestIMDv3NAMD(IMDv3IntegrationTest): + @pytest.fixture(params=[NAMD_CONF_NST_1, NAMD_CONF_NST_8]) + def inp(self, request): + return request.param + + @pytest.fixture() + def simulation_command(self, inp): + return f"namd3 {Path(inp).name}" + + @pytest.fixture() + def input_files(self, inp): + return [NAMD_TOPOL, inp, NAMD_PARAMS, NAMD_PSF] + + @pytest.fixture() + def topol(self): + return Path(NAMD_TOPOL).name + @pytest.fixture() - def command(self): - return ( - f"cp {NAMD_PARAMS} {NAMD_PSF} {NAMD_TOPOL} . && namd3 {NAMD_CONF}" + def true_u(self, topol, imd_u, tmp_path): + u = mda.Universe( + (tmp_path / topol), + (tmp_path / "alanin.dcd"), ) + yield u @pytest.fixture() - def match_string(self): - return "INTERACTIVE MD AWAITING CONNECTION" + def true_u_vel(self, topol, imd_u, tmp_path): + u = mda.Universe( + (tmp_path / topol), + (tmp_path / "alanin.vel.dcd"), + ) + yield u + + @pytest.fixture() + def true_u_force(self, topol, imd_u, tmp_path): + u = mda.Universe( + (tmp_path / topol), + (tmp_path / "alanin.force.dcd"), + ) + yield u + + # @pytest.fixture() + # def match_string(self): + # return "INTERACTIVE MD AWAITING CONNECTION" @pytest.fixture() def first_frame(self): return 0 - @pytest.fixture() - def universe(self): - return mda.Universe( - NAMD_TOPOL, - NAMD_TRAJ, - ) + # Compare coords, box, time, dt, step + def test_compare_imd_to_true_traj(self, imd_u, true_u, first_frame): + for i in range(first_frame, len(true_u.trajectory)): + assert_allclose( + true_u.trajectory[i].time, + imd_u.trajectory[i - first_frame].time, + atol=1e-03, + ) + assert_allclose( + true_u.trajectory[i].dt, + imd_u.trajectory[i - first_frame].dt, + atol=1e-03, + ) + assert_allclose( + true_u.trajectory[i].data["step"], + imd_u.trajectory[i - first_frame].data["step"], + ) + assert_allclose_with_logging( + true_u.trajectory[i].dimensions, + imd_u.trajectory[i - first_frame].dimensions, + atol=1e-03, + ) + assert_allclose_with_logging( + true_u.trajectory[i].positions, + imd_u.trajectory[i - first_frame].positions, + atol=1e-03, + ) + + # Compare velocities + def test_compare_imd_to_true_traj_vel( + self, imd_u, true_u_vel, first_frame + ): + for i in range(first_frame, len(true_u_vel.trajectory)): + assert_allclose_with_logging( + true_u_vel.trajectory[i].positions, + imd_u.trajectory[i - first_frame].velocities, + atol=1e-03, + ) + + # Compare forces + def test_compare_imd_to_true_traj_forces( + self, imd_u, true_u_force, first_frame + ): + for i in range(first_frame, len(true_u_force.trajectory)): + assert_allclose_with_logging( + true_u_force.trajectory[i].positions, + imd_u.trajectory[i - first_frame].forces, + atol=1e-03, + ) diff --git a/pyproject.toml b/pyproject.toml index ff7ef84..506d12d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ test = [ "pytest-xdist>=2.5", "pytest-cov>=3.0", "MDAnalysisTests>=2.7.0", + "docker-py", ] doc = [ "sphinx",