diff --git a/imdclient/IMDClient.py b/imdclient/IMDClient.py index 46f8942..60484d4 100644 --- a/imdclient/IMDClient.py +++ b/imdclient/IMDClient.py @@ -59,6 +59,11 @@ class IMDClient: If True, the client will attempt to change the simulation engine's waiting behavior to non-blocking after the client disconnects. If False, the client will attempt to change it to blocking. If None, the client will not attempt to change the simulation engine's behavior. + transmission_rate : int, optional [``None``] + IMD transmission rate to be set after client send go signal. + This parameter is only set to the server when using IMDv2. Default behavior is to not set the transmission rate. + This parameter is useful when running GROMACS with IMDv2, where transmission rate can only be set via the client. + IMDv2 implementations in LAMMPS and NAMD support setting the transmission rate via relevant input file parameters. **kwargs : dict (optional) Additional keyword arguments to pass to the :class:`BaseIMDProducer` and :class:`IMDFrameBuffer` """ @@ -71,6 +76,7 @@ def __init__( socket_bufsize=None, multithreaded=True, continue_after_disconnect=None, + transmission_rate=None, **kwargs, ): @@ -120,6 +126,9 @@ def __init__( self._go() + if transmission_rate is not None and self._imdsinfo.version == 2: + self._trate(transmission_rate) + if self._multithreaded: # Disconnect MUST occur. This covers typical cases (Python, IPython interpreter) signal.signal(signal.SIGINT, self.signal_handler) @@ -293,13 +302,13 @@ def _await_IMD_handshake(self) -> IMDSessionInfo: sinfo = IMDSessionInfo( version=ver, endianness=end, - wrapped_coords=False, time=False, energies=True, box=False, positions=True, velocities=False, forces=False, + wrapped_coords=False, ) elif ver == 3: @@ -320,9 +329,17 @@ def _await_IMD_handshake(self) -> IMDSessionInfo: return sinfo + def _trate(self, rate): + """ + Send a trate packet to the server to set transmission rate. + """ + trate = create_header_bytes(IMDHeaderType.IMD_TRATE, rate) + self._conn.sendall(trate) + logger.debug("IMDClient: Sent transmission rate %s", rate) + def _go(self): """ - Send a go packet to the client to start the simulation + Send a go packet to the server to start the simulation and begin receiving data. """ go = create_header_bytes(IMDHeaderType.IMD_GO, 0) @@ -579,9 +596,10 @@ def _parse_imdframe(self): # Even if they are sent, energies might not be sent every frame # cache the last energies received - # Either receive energies + positions or just positions + # Consume any leading energy packets first, then handle positions. header = self._get_header() - if header.type == IMDHeaderType.IMD_ENERGIES and header.length == 1: + leading_energies = 0 + while header.type == IMDHeaderType.IMD_ENERGIES and header.length == 1: self._imdsinfo.energies = True self._read(self._energies) self._imdf.energies.update( @@ -589,20 +607,22 @@ def _parse_imdframe(self): ) self._prev_energies = self._imdf.energies - self._expect_header( - IMDHeaderType.IMD_FCOORDS, expected_value=self._n_atoms - ) - self._read(self._positions) - np.copyto( - self._imdf.positions, - np.frombuffer( - self._positions, dtype=f"{self._imdsinfo.endianness}f" - ).reshape((self._n_atoms, 3)), + leading_energies += 1 + + header = self._get_header() + + if leading_energies == 0 or leading_energies > 1: + logger.warning( + f"IMDProducer: Received {leading_energies} leading IMDv2 energy packets before coordinates, energy values may be out of sync with coordinates" ) - elif ( - header.type == IMDHeaderType.IMD_FCOORDS - and header.length == self._n_atoms - ): + + if header.type == IMDHeaderType.IMD_FCOORDS: + # check if the number of atoms is correct + if header.length != self._n_atoms: + raise RuntimeError( + f"IMDProducer: Expected n_atoms value {self._n_atoms}, got {header.length}. " + + "Ensure you are using the correct topology file." + ) # If we received positions but no energies # use the last energies received if self._prev_energies is not None: @@ -617,7 +637,9 @@ def _parse_imdframe(self): ).reshape((self._n_atoms, 3)), ) else: - raise RuntimeError("IMDProducer: Unexpected packet type or length") + raise RuntimeError( + f"IMDProducer: Unexpected packet type {header.type.name}" + ) def _pause(self): logger.debug( diff --git a/imdclient/data/gromacs/md/gromacs_v2_nst1.mdp b/imdclient/data/gromacs/md/gromacs_v2_nst1.mdp new file mode 100644 index 0000000..c2509a6 --- /dev/null +++ b/imdclient/data/gromacs/md/gromacs_v2_nst1.mdp @@ -0,0 +1,51 @@ +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 = 1 ; save coordinates every 1 fs +nstvout = 1 ; save velocities every 1 fs +nstfout = 1 ; save forces every 1 fs +nstenergy = 1 ; save energies every 1 fs +nstlog = 10 +; 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 = 1 +IMD-version = 2 diff --git a/imdclient/data/gromacs/md/gromacs_v2_nst8.mdp b/imdclient/data/gromacs/md/gromacs_v2_nst8.mdp new file mode 100644 index 0000000..763b8e9 --- /dev/null +++ b/imdclient/data/gromacs/md/gromacs_v2_nst8.mdp @@ -0,0 +1,51 @@ +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 = 2 diff --git a/imdclient/data/lammps/md/lammps_v2_nst_1.in b/imdclient/data/lammps/md/lammps_v2_nst_1.in new file mode 100644 index 0000000..9f7d82d --- /dev/null +++ b/imdclient/data/lammps/md/lammps_v2_nst_1.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 1 lammps_trj.h5md position +dump_modify h5md1 unwrap no + +## IMD settings +# https://docs.lammps.org/fix_imd.html +fix 2 all imd 8888 version 2 nowait off trate 1 + +## 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/lammps/md/lammps_v2_nst_8.in b/imdclient/data/lammps/md/lammps_v2_nst_8.in new file mode 100644 index 0000000..bc583a2 --- /dev/null +++ b/imdclient/data/lammps/md/lammps_v2_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 +dump_modify h5md1 unwrap no + +## IMD settings +# https://docs.lammps.org/fix_imd.html +fix 2 all imd 8888 version 2 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/lammps/md/lammps_v3_nst_1.in b/imdclient/data/lammps/md/lammps_v3_nst_1.in index f848785..c1ac4cd 100644 --- a/imdclient/data/lammps/md/lammps_v3_nst_1.in +++ b/imdclient/data/lammps/md/lammps_v3_nst_1.in @@ -57,7 +57,7 @@ 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 +fix 2 all imd 8888 version 3 unwrap off nowait off trate 1 time on box on coordinates on velocities on forces on ## Run MD sim run 100 diff --git a/imdclient/data/lammps/md/lammps_v3_nst_8.in b/imdclient/data/lammps/md/lammps_v3_nst_8.in index 8336fb9..5d91267 100644 --- a/imdclient/data/lammps/md/lammps_v3_nst_8.in +++ b/imdclient/data/lammps/md/lammps_v3_nst_8.in @@ -57,7 +57,7 @@ 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 +fix 2 all imd 8888 version 3 unwrap off nowait off trate 8 time on box on coordinates on velocities on forces on ## Run MD sim run 100 diff --git a/imdclient/data/namd/md/namd_v2_nst_1.namd b/imdclient/data/namd/md/namd_v2_nst_1.namd new file mode 100644 index 0000000..70c8007 --- /dev/null +++ b/imdclient/data/namd/md/namd_v2_nst_1.namd @@ -0,0 +1,52 @@ +# 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 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 + +#langevin on +#langevinTemp 300.0 +#langevincol O + +#constraints on + +#fma on + +seed 12345 + +IMDon yes +IMDport 8888 +IMDfreq 1 +IMDwait on +IMDversion 2 \ No newline at end of file diff --git a/imdclient/data/namd/md/namd_v2_nst_8.namd b/imdclient/data/namd/md/namd_v2_nst_8.namd new file mode 100644 index 0000000..680070d --- /dev/null +++ b/imdclient/data/namd/md/namd_v2_nst_8.namd @@ -0,0 +1,52 @@ +# 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 2 \ No newline at end of file diff --git a/imdclient/tests/base.py b/imdclient/tests/base.py index 5484ec1..75c25c7 100644 --- a/imdclient/tests/base.py +++ b/imdclient/tests/base.py @@ -10,6 +10,8 @@ import docker import MDAnalysis as mda +from MDAnalysis.transformations.wrap import wrap + from .utils import get_free_port from .minimalreader import MinimalReader @@ -59,7 +61,7 @@ def assert_allclose_with_logging(a, b, rtol=1e-07, atol=0, equal_nan=False): print("All values are within tolerance.") -class IMDv3IntegrationTest: +class IMDIntegrationTest: @pytest.fixture() def container_name(self): @@ -69,6 +71,10 @@ def container_name(self): def setup_command(self): return None + @pytest.fixture() + def post_simulation_command(self): + return None + @pytest.fixture() def port(self): yield get_free_port() @@ -80,6 +86,7 @@ def docker_client( input_files, setup_command, simulation_command, + post_simulation_command, port, container_name, ): @@ -100,6 +107,8 @@ def docker_client( cmdstring += " && " + setup_command cmdstring += " && " + simulation_command + if post_simulation_command is not None: + cmdstring += " && " + post_simulation_command # Start the container, mount tmp_path, run simulation container = docker_client.containers.run( @@ -117,28 +126,49 @@ def docker_client( # a container time.sleep(30) - yield + yield container + try: container.stop() except docker.errors.NotFound: pass @pytest.fixture() - def imd_u(self, docker_client, topol, tmp_path, port): + def first_frame(self): + return 0 + + @pytest.fixture() + def imd_nst(self): + return None + + @pytest.fixture() + def imd_u(self, docker_client, topol, tmp_path, port, imd_nst): n_atoms = mda.Universe(tmp_path / topol).atoms.n_atoms u = MinimalReader( - f"imd://localhost:{port}", n_atoms=n_atoms, process_stream=True + f"imd://localhost:{port}", + n_atoms=n_atoms, + process_stream=True, + transmission_rate=imd_nst, ) yield u @pytest.fixture() - def true_u(self, topol, traj, imd_u, tmp_path): + def true_u(self, topol, traj, imd_u, tmp_path, docker_client, first_frame): + # imd_u finishes when the IMD stream closes; wait for any post-processing in post_simulation_command + # in the container before reading the reference trajectory from disk. + docker_client.wait() u = mda.Universe( (tmp_path / topol), (tmp_path / traj), ) + if not imd_u.imdsinfo.wrapped_coords: + u.trajectory.add_transformations(wrap(u.atoms, compound="atoms")) + imd_u._wrap_trajectory(u, first_frame) yield u + +class IMDv3IntegrationTest(IMDIntegrationTest): + def test_compare_imd_to_true_traj(self, imd_u, true_u, first_frame, dt): for i in range(first_frame, len(true_u.trajectory)): @@ -233,3 +263,15 @@ def test_wait_after_disconnect(self, docker_client, topol, tmp_path, port): atom_style="id type x y z", ).atoms.n_atoms u = MinimalReader(f"imd://localhost:{port}", n_atoms=n_atoms) + + +class IMDv2IntegrationTest(IMDIntegrationTest): + + 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_with_logging( + true_u.trajectory[i].positions, + imd_u.trajectory[i - first_frame].positions, + atol=1e-03, + ) diff --git a/imdclient/tests/datafiles.py b/imdclient/tests/datafiles.py index 830a043..e50adfb 100644 --- a/imdclient/tests/datafiles.py +++ b/imdclient/tests/datafiles.py @@ -10,17 +10,23 @@ __all__ = [ "LAMMPS_TOPOL", - "LAMMPS_IN_NST_1", - "LAMMPS_IN_NST_8", + "LAMMPS_IN_V3_NST_1", + "LAMMPS_IN_V3_NST_8", + "LAMMPS_IN_V2_NST_1", + "LAMMPS_IN_V2_NST_8", "GROMACS_TRAJ", "GROMACS_MDP", "GROMACS_TOP", "GROMACS_GRO", - "GROMACS_MDP_NST_1", - "GROMACS_MDP_NST_8", + "GROMACS_MDP_V3_NST_1", + "GROMACS_MDP_V3_NST_8", + "GROMACS_MDP_V2_NST_1", + "GROMACS_MDP_V2_NST_8", "NAMD_TOPOL", - "NAMD_CONF_NST_1", - "NAMD_CONF_NST_8", + "NAMD_CONF_V3_NST_1", + "NAMD_CONF_V3_NST_8", + "NAMD_CONF_V2_NST_1", + "NAMD_CONF_V2_NST_8", "NAMD_PARAMS", "NAMD_PSF", ] @@ -31,26 +37,48 @@ _data_ref = resources.files("imdclient.data") LAMMPS_TOPOL = (_data_ref / "lammps" / "md" / "lammps_topol.data").as_posix() -LAMMPS_IN_NST_1 = ( +LAMMPS_IN_V3_NST_1 = ( _data_ref / "lammps" / "md" / "lammps_v3_nst_1.in" ).as_posix() -LAMMPS_IN_NST_8 = ( +LAMMPS_IN_V3_NST_8 = ( _data_ref / "lammps" / "md" / "lammps_v3_nst_8.in" ).as_posix() +LAMMPS_IN_V2_NST_1 = ( + _data_ref / "lammps" / "md" / "lammps_v2_nst_1.in" +).as_posix() +LAMMPS_IN_V2_NST_8 = ( + _data_ref / "lammps" / "md" / "lammps_v2_nst_8.in" +).as_posix() GROMACS_GRO = (_data_ref / "gromacs" / "md" / "gromacs_struct.gro").as_posix() -GROMACS_MDP_NST_1 = ( +GROMACS_MDP_V3_NST_1 = ( _data_ref / "gromacs" / "md" / "gromacs_v3_nst1.mdp" ).as_posix() -GROMACS_MDP_NST_8 = ( +GROMACS_MDP_V3_NST_8 = ( _data_ref / "gromacs" / "md" / "gromacs_v3_nst8.mdp" ).as_posix() +GROMACS_MDP_V2_NST_1 = ( + _data_ref / "gromacs" / "md" / "gromacs_v2_nst1.mdp" +).as_posix() +GROMACS_MDP_V2_NST_8 = ( + _data_ref / "gromacs" / "md" / "gromacs_v2_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_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_CONF_V3_NST_1 = ( + _data_ref / "namd" / "md" / "namd_v3_nst_1.namd" +).as_posix() +NAMD_CONF_V3_NST_8 = ( + _data_ref / "namd" / "md" / "namd_v3_nst_8.namd" +).as_posix() +NAMD_CONF_V2_NST_1 = ( + _data_ref / "namd" / "md" / "namd_v2_nst_1.namd" +).as_posix() +NAMD_CONF_V2_NST_8 = ( + _data_ref / "namd" / "md" / "namd_v2_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/minimalreader.py b/imdclient/tests/minimalreader.py index 247808b..5f3f28f 100644 --- a/imdclient/tests/minimalreader.py +++ b/imdclient/tests/minimalreader.py @@ -1,7 +1,9 @@ import logging import copy +import numpy as np from MDAnalysis.coordinates import core +from MDAnalysis.lib import distances from imdclient.IMDClient import IMDClient from imdclient.utils import parse_host_port @@ -48,6 +50,10 @@ def __init__(self, filename, n_atoms, process_stream=False, **kwargs): if process_stream: self._process_stream() + @property + def imdsinfo(self): + return self._imdclient.get_imdsessioninfo() + def _read_next_frame(self): try: imd_frame = self._imdclient.get_imdframe() @@ -58,13 +64,16 @@ def _read_next_frame(self): self.imd_frame = imd_frame # Modify the box dimensions to be triclinic - self._modify_box_dimesions() + self._modify_box_dimensions() logger.debug(f"MinimalReader: Loaded frame {self._frame}") return self.imd_frame - def _modify_box_dimesions(self): + def _modify_box_dimensions(self): + if self.imd_frame.box is None: + self.imd_frame.dimensions = None + return self.imd_frame.dimensions = core.triclinic_box(*self.imd_frame.box) def _process_stream(self): @@ -79,6 +88,27 @@ def _process_stream(self): except EOFError: break + def _wrap_frame(self, frame, box): + if box is None: + return + frame.positions = distances.apply_PBC( + np.asarray(frame.positions, dtype=np.float32), + box, + ) + + def _wrap_trajectory(self, true_u=None, first_frame=0): + """Wrap each stored IMD frame into the primary box (per atom).""" + if true_u is None: + for frame in self.trajectory: + self._wrap_frame(frame, frame.dimensions) + return + + for i in range(first_frame, len(true_u.trajectory)): + self._wrap_frame( + self.trajectory[i - first_frame], + true_u.trajectory[i].dimensions, + ) + def close(self): """Gracefully shut down the reader. Stops the producer thread.""" logger.debug("MinimalReader: close() called") diff --git a/imdclient/tests/server.py b/imdclient/tests/server.py index 406cdd7..708e333 100644 --- a/imdclient/tests/server.py +++ b/imdclient/tests/server.py @@ -202,6 +202,10 @@ def expect_packet(self, packet_type, expected_length=None): f"Expected packet length {expected_length}, got {header.length}" ) + def expect_no_packet(self, timeout=0): + if sock_contains_data(self.conn, timeout): + raise ValueError("Expected no packet here") + def disconnect(self): # send EOF to the client, marking end of stream self.conn.shutdown(socket.SHUT_RDWR) diff --git a/imdclient/tests/test_gromacs.py b/imdclient/tests/test_gromacs.py index 00e5a89..152769e 100644 --- a/imdclient/tests/test_gromacs.py +++ b/imdclient/tests/test_gromacs.py @@ -4,12 +4,14 @@ import pytest -from .base import IMDv3IntegrationTest +from .base import IMDv2IntegrationTest, IMDv3IntegrationTest from .datafiles import ( GROMACS_GRO, GROMACS_TOP, - GROMACS_MDP_NST_1, - GROMACS_MDP_NST_8, + GROMACS_MDP_V3_NST_1, + GROMACS_MDP_V3_NST_8, + GROMACS_MDP_V2_NST_1, + GROMACS_MDP_V2_NST_8, ) logger = logging.getLogger("imdclient.IMDClient") @@ -22,11 +24,7 @@ logger.setLevel(logging.DEBUG) -class TestIMDv3Gromacs(IMDv3IntegrationTest): - - @pytest.fixture(params=[GROMACS_MDP_NST_1, GROMACS_MDP_NST_8]) - def mdp(self, request): - return request.param +class IMDGromacsTest: @pytest.fixture() def setup_command(self, mdp): @@ -58,10 +56,26 @@ def dt(self, mdp): return float(match.group(1)) raise ValueError(f"No dt found in {mdp}") - # @pytest.fixture() - # def match_string(self): - # return "IMD: Will wait until I have a connection and IMD_GO orders." + +class TestIMDv3Gromacs(IMDGromacsTest, IMDv3IntegrationTest): + + @pytest.fixture(params=[GROMACS_MDP_V3_NST_1, GROMACS_MDP_V3_NST_8]) + def mdp(self, request): + return request.param + + +class TestIMDv2Gromacs(IMDGromacsTest, IMDv2IntegrationTest): + + @pytest.fixture(params=[GROMACS_MDP_V2_NST_1, GROMACS_MDP_V2_NST_8]) + def mdp(self, request): + return request.param @pytest.fixture() - def first_frame(self): - return 0 + def imd_nst(self, mdp): + pattern = re.compile(r"^\s*IMD-nst\s*=\s*(\S+)") + with open(mdp, "r") as file: + for line in file: + match = pattern.match(line) + if match: + return int(match.group(1)) + raise ValueError(f"No IMD-nst found in {mdp}") diff --git a/imdclient/tests/test_imdclient.py b/imdclient/tests/test_imdclient.py index 296d5c6..fa922a8 100644 --- a/imdclient/tests/test_imdclient.py +++ b/imdclient/tests/test_imdclient.py @@ -4,26 +4,29 @@ import sys import time -import pytest -from numpy.testing import ( - assert_allclose, -) import MDAnalysis as mda from MDAnalysisTests.datafiles import ( - COORDINATES_TOPOLOGY, COORDINATES_H5MD, + COORDINATES_TOPOLOGY, ) +from numpy.testing import assert_allclose +import pytest from imdclient.IMDClient import ( IMDFrameBuffer, imdframe_memsize, IMDClient, ) -from imdclient.IMDProtocol import IMDHeaderType +from imdclient.IMDProtocol import ( + IMDHeaderType, + create_energy_bytes, + create_header_bytes, +) +from .server import InThreadIMDServer from .utils import ( + create_default_imdsinfo_v2, create_default_imdsinfo_v3, ) -from .server import InThreadIMDServer logger = logging.getLogger("imdclient.IMDClient") @@ -49,15 +52,14 @@ ] -class TestIMDClientV3: - +class IMDClientTest: @pytest.fixture def universe(self): return mda.Universe(COORDINATES_TOPOLOGY, COORDINATES_H5MD) @pytest.fixture def imdsinfo(self): - return create_default_imdsinfo_v3() + return create_default_imdsinfo_v2() @pytest.fixture def server_client(self, universe, imdsinfo): @@ -114,20 +116,52 @@ def server_client_incorrect_atoms(self, server_client, universe): server, client = server_client(n_atoms=universe.trajectory.n_atoms + 1) yield server, client - def test_traj_unchanged(self, server_client_endianness, universe): + def test_traj_unchanged(self, server_client_endianness, universe, imdsinfo): server, client = server_client_endianness server.send_frames(0, 5) for i in range(5): imdf = client.get_imdframe() - assert_allclose(universe.trajectory[i].time, imdf.time) - assert_allclose(universe.trajectory[i].dt, imdf.dt) - assert_allclose(universe.trajectory[i].data["step"], imdf.step) - assert_allclose(universe.trajectory[i].positions, imdf.positions) - assert_allclose(universe.trajectory[i].velocities, imdf.velocities) - assert_allclose(universe.trajectory[i].forces, imdf.forces) - assert_allclose( - universe.trajectory[i].triclinic_dimensions, imdf.box - ) + if imdsinfo.time: + assert_allclose(universe.trajectory[i].time, imdf.time) + assert_allclose(universe.trajectory[i].dt, imdf.dt) + assert_allclose(universe.trajectory[i].data["step"], imdf.step) + if imdsinfo.box: + assert_allclose( + universe.trajectory[i].triclinic_dimensions, imdf.box + ) + if imdsinfo.positions: + assert_allclose( + universe.trajectory[i].positions, imdf.positions + ) + if imdsinfo.velocities: + assert_allclose( + universe.trajectory[i].velocities, imdf.velocities + ) + if imdsinfo.forces: + assert_allclose(universe.trajectory[i].forces, imdf.forces) + + def test_incorrect_atom_count( + self, server_client_incorrect_atoms, universe + ): + server, client = server_client_incorrect_atoms + + server.send_frame(0) + + with pytest.raises(EOFError) as exc_info: + client.get_imdframe() + + error_msg = str(exc_info.value) + assert ( + f"Expected n_atoms value {universe.atoms.n_atoms + 1}" in error_msg + ) + assert f"got {universe.atoms.n_atoms}" in error_msg + assert "Ensure you are using the correct topology file" in error_msg + + +class TestIMDClientV3(IMDClientTest): + @pytest.fixture + def imdsinfo(self): + return create_default_imdsinfo_v3() def test_pause_resume_continue(self, server_client_two_frame_buf): server, client = server_client_two_frame_buf @@ -249,23 +283,6 @@ def test_timeout_when_exceeded(self, server_client, timeout_val): assert TimeoutError in exception_chain - def test_incorrect_atom_count( - self, server_client_incorrect_atoms, universe - ): - server, client = server_client_incorrect_atoms - - server.send_frame(0) - - with pytest.raises(EOFError) as exc_info: - client.get_imdframe() - - error_msg = str(exc_info.value) - assert ( - f"Expected n_atoms value {universe.atoms.n_atoms + 1}" in error_msg - ) - assert f"got {universe.atoms.n_atoms}" in error_msg - assert "Ensure you are using the correct topology file" in error_msg - def test_single_threaded_client_reads_frame_and_eof( self, server_client, universe ): @@ -315,12 +332,201 @@ def _raise_valueerror(): with pytest.raises(RuntimeError, match="An unexpected error occurred"): client._producer._get_imdframe() + def test_trate_not_sent_for_v3(self, universe, imdsinfo): + server = InThreadIMDServer(universe.trajectory) + server.set_imdsessioninfo(imdsinfo) + server.handshake_sequence("localhost", first_frame=False) + client = IMDClient( + "localhost", + server.port, + universe.atoms.n_atoms, + transmission_rate=8, + ) + server.join_accept_thread() + server.expect_no_packet() + client.stop() + server.cleanup() -class TestIMDClientV3ContextManager: - @pytest.fixture - def universe(self): - return mda.Universe(COORDINATES_TOPOLOGY, COORDINATES_H5MD) +class TestIMDClientV2(IMDClientTest): + @pytest.mark.parametrize("rate", [1, 8]) + def test_trate_sent_after_go_for_v2(self, universe, imdsinfo, rate): + server = InThreadIMDServer(universe.trajectory) + server.set_imdsessioninfo(imdsinfo) + server.handshake_sequence("localhost", first_frame=False) + client = IMDClient( + "localhost", + server.port, + universe.atoms.n_atoms, + transmission_rate=rate, + ) + server.join_accept_thread() + server.expect_packet(IMDHeaderType.IMD_TRATE, expected_length=rate) + client.stop() + server.cleanup() + + def test_pause_pause_continue(self, server_client_two_frame_buf): + server, client = server_client_two_frame_buf + server.send_frames(0, 2) + # Client's buffer is filled; client should send pause. + server.expect_packet(IMDHeaderType.IMD_PAUSE) + # Empty buffer. + client.get_imdframe() + # Only the second call actually frees buffer memory. + client.get_imdframe() + # IMDv2 uses IMD_PAUSE for both pause and unpause. + server.expect_packet(IMDHeaderType.IMD_PAUSE) + server.send_frame(1) + client.get_imdframe() + + def test_pause_pause_disconnect(self, server_client_two_frame_buf): + """Client pauses because buffer is full, empties buffer and attempts to + unpause with a second IMD_PAUSE, but simulation has already ended and + raises EOF.""" + server, client = server_client_two_frame_buf + server.send_frames(0, 2) + server.expect_packet(IMDHeaderType.IMD_PAUSE) + client.get_imdframe() + client.get_imdframe() + # IMDv2 uses IMD_PAUSE for both pause and unpause. + server.expect_packet(IMDHeaderType.IMD_PAUSE) + # Simulation is over; client should raise EOF. + server.disconnect() + with pytest.raises(EOFError): + client.get_imdframe() + + def test_pause_pause_no_disconnect(self, server_client_two_frame_buf): + """Client pauses because buffer is full, empties buffer and attempts to + unpause with a second IMD_PAUSE, but simulation has already ended (without + disconnecting) and raises EOF.""" + server, client = server_client_two_frame_buf + server.send_frames(0, 2) + server.expect_packet(IMDHeaderType.IMD_PAUSE) + client.get_imdframe() + client.get_imdframe() + # IMDv2 uses IMD_PAUSE for both pause and unpause. + server.expect_packet(IMDHeaderType.IMD_PAUSE) + # Simulation is over; client should raise EOF. + with pytest.raises(EOFError): + client.get_imdframe() + # Server should receive disconnect from client (though it doesn't have to do anything). + server.expect_packet(IMDHeaderType.IMD_DISCONNECT) + + def test_reads_multiple_leading_energies_before_coords( + self, server_client, caplog + ): + server, client = server_client() + + with caplog.at_level(logging.WARNING, logger="imdclient.IMDClient"): + endianness = client.get_imdsessioninfo().endianness + # Send >3 leading energy packets before coordinates. + for i in range(4): + energy_header = create_header_bytes( + IMDHeaderType.IMD_ENERGIES, 1 + ) + energies = create_energy_bytes( + i, + i + 1, + i + 2, + i + 3, + i + 4, + i + 5, + i + 6, + i + 7, + i + 8, + i + 9, + endianness, + ) + server.conn.sendall(energy_header + energies) + + pos_header = create_header_bytes( + IMDHeaderType.IMD_FCOORDS, server.traj.n_atoms + ) + pos = universe_frame0 = server.traj[0].positions + pos_bytes = pos.astype(f"{endianness}f", copy=False).tobytes() + server.conn.sendall(pos_header + pos_bytes) + + imdf = client.get_imdframe() + + assert imdf.energies is not None + assert imdf.energies["step"] == 3 + assert_allclose(universe_frame0, imdf.positions) + assert any( + "Received 4 leading IMDv2 energy packets" in rec.message + for rec in caplog.records + ) + + def test_reads_no_leading_energy_packets(self, server_client, caplog): + server, client = server_client() + + with caplog.at_level(logging.WARNING, logger="imdclient.IMDClient"): + endianness = client.get_imdsessioninfo().endianness + # Send coords only, no leading energy packet. + pos_header = create_header_bytes( + IMDHeaderType.IMD_FCOORDS, server.traj.n_atoms + ) + pos = server.traj[0].positions + pos_bytes = pos.astype(f"{endianness}f", copy=False).tobytes() + server.conn.sendall(pos_header + pos_bytes) + + imdf = client.get_imdframe() + + assert imdf.energies is None + assert_allclose(pos, imdf.positions) + assert any( + "Received 0 leading IMDv2 energy packets" in rec.message + for rec in caplog.records + ) + + def test_uses_previous_energies_when_frame_has_coords_only( + self, server_client + ): + server, client = server_client() + + # First frame with energies sets the cache. + server.send_frame(0) + first = client.get_imdframe() + assert first.energies is not None + prev_step = first.energies["step"] + + # Second frame: send only coords; should reuse previous energies. + endianness = client.get_imdsessioninfo().endianness + pos_header = create_header_bytes( + IMDHeaderType.IMD_FCOORDS, server.traj.n_atoms + ) + pos = server.traj[1].positions + pos_bytes = pos.astype(f"{endianness}f", copy=False).tobytes() + server.conn.sendall(pos_header + pos_bytes) + + second = client.get_imdframe() + assert second.energies is not None + assert second.energies["step"] == prev_step + assert_allclose(pos, second.positions) + + @pytest.mark.parametrize( + "packet_type", + [ + IMDHeaderType.IMD_TIME, + IMDHeaderType.IMD_BOX, + IMDHeaderType.IMD_VELOCITIES, + IMDHeaderType.IMD_FORCES, + ], + ) + def test_unexpected_packet_type_in_v2(self, server_client, packet_type): + """IMDv2 rejects v3-only packet types received before coordinates.""" + server, client = server_client() + + server.conn.sendall(create_header_bytes(packet_type, 1)) + + with pytest.raises(EOFError) as exc_info: + client.get_imdframe() + + assert f"Unexpected packet type {packet_type.name}" in str( + exc_info.value + ) + + +class TestIMDClientV3ContextManager(IMDClientTest): @pytest.fixture def imdsinfo(self): return create_default_imdsinfo_v3() @@ -337,13 +543,10 @@ def test_context_manager_traj_unchanged(self, server, universe): i = 0 with IMDClient( - "localhost", - server.port, - universe.trajectory.n_atoms, + "localhost", server.port, universe.trajectory.n_atoms ) as client: server.send_frames(0, 5) while i < 5: - imdf = client.get_imdframe() assert_allclose(universe.trajectory[i].time, imdf.time) assert_allclose(universe.trajectory[i].dt, imdf.dt) diff --git a/imdclient/tests/test_lammps.py b/imdclient/tests/test_lammps.py index d033f96..37d76c2 100644 --- a/imdclient/tests/test_lammps.py +++ b/imdclient/tests/test_lammps.py @@ -4,10 +4,17 @@ import pytest import MDAnalysis as mda +from MDAnalysis.transformations.wrap import wrap from .minimalreader import MinimalReader -from .base import IMDv3IntegrationTest -from .datafiles import LAMMPS_TOPOL, LAMMPS_IN_NST_1, LAMMPS_IN_NST_8 +from .base import IMDv2IntegrationTest, IMDv3IntegrationTest +from .datafiles import ( + LAMMPS_TOPOL, + LAMMPS_IN_V3_NST_1, + LAMMPS_IN_V3_NST_8, + LAMMPS_IN_V2_NST_1, + LAMMPS_IN_V2_NST_8, +) logger = logging.getLogger("imdclient.IMDClient") file_handler = logging.FileHandler("lammps_test.log") @@ -19,11 +26,7 @@ logger.setLevel(logging.DEBUG) -class TestIMDv3Lammps(IMDv3IntegrationTest): - - @pytest.fixture(params=[LAMMPS_IN_NST_1, LAMMPS_IN_NST_8]) - def inp(self, request): - return request.param +class IMDLammpsTest: @pytest.fixture() def simulation_command(self, inp): @@ -45,13 +48,6 @@ def input_files(self, inp): # def match_string(self): # return "Waiting for IMD connection on port 8888" - @pytest.fixture() - def first_frame(self, inp): - if inp == LAMMPS_IN_NST_1: - return 1 - else: - return 0 - @pytest.fixture() def dt(self, inp): pattern = re.compile(r"^\s*timestep\s*(\S+)") @@ -62,15 +58,19 @@ def dt(self, inp): return float(match.group(1)) raise ValueError(f"No dt found in {inp}") - # This must wait until after imd stream has ended + # This must wait until after imd stream and post-simulation processing has ended @pytest.fixture() - def true_u(self, topol, traj, imd_u, tmp_path): + def true_u(self, topol, traj, imd_u, tmp_path, docker_client, first_frame): + docker_client.wait() u = mda.Universe( (tmp_path / topol), (tmp_path / traj), atom_style="id type x y z", convert_units=False, ) + if not imd_u.imdsinfo.wrapped_coords: + u.trajectory.add_transformations(wrap(u.atoms, compound="atoms")) + imd_u._wrap_trajectory(u, first_frame) yield u @pytest.fixture() @@ -84,3 +84,31 @@ def imd_u(self, docker_client, topol, tmp_path, port): f"imd://localhost:{port}", n_atoms=n_atoms, process_stream=True ) yield u + + +class TestIMDv3Lammps(IMDLammpsTest, IMDv3IntegrationTest): + + @pytest.fixture(params=[LAMMPS_IN_V3_NST_1, LAMMPS_IN_V3_NST_8]) + def inp(self, request): + return request.param + + @pytest.fixture() + def first_frame(self, inp): + if inp == LAMMPS_IN_V3_NST_1: + return 1 + else: + return 0 + + +class TestIMDv2Lammps(IMDLammpsTest, IMDv2IntegrationTest): + + @pytest.fixture(params=[LAMMPS_IN_V2_NST_1, LAMMPS_IN_V2_NST_8]) + def inp(self, request): + return request.param + + @pytest.fixture() + def first_frame(self, inp): + if inp == LAMMPS_IN_V2_NST_1: + return 1 + else: + return 0 diff --git a/imdclient/tests/test_namd.py b/imdclient/tests/test_namd.py index 06b4930..4c5290b 100644 --- a/imdclient/tests/test_namd.py +++ b/imdclient/tests/test_namd.py @@ -7,12 +7,19 @@ assert_allclose, ) import MDAnalysis as mda +from MDAnalysis.transformations.wrap import wrap -from .base import IMDv3IntegrationTest, assert_allclose_with_logging +from .base import ( + IMDv2IntegrationTest, + IMDv3IntegrationTest, + assert_allclose_with_logging, +) from .datafiles import ( NAMD_TOPOL, - NAMD_CONF_NST_1, - NAMD_CONF_NST_8, + NAMD_CONF_V3_NST_1, + NAMD_CONF_V3_NST_8, + NAMD_CONF_V2_NST_1, + NAMD_CONF_V2_NST_8, NAMD_PARAMS, NAMD_PSF, ) @@ -27,16 +34,12 @@ logger.setLevel(logging.DEBUG) -class TestIMDv3NAMD(IMDv3IntegrationTest): +class IMDNAMDTest: @pytest.fixture() def container_name(self): return "ghcr.io/becksteinlab/streaming-namd-docker:main-common-cpu" - @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}" @@ -62,13 +65,27 @@ def dt(self, inp): raise ValueError(f"No dt found in {inp}") @pytest.fixture() - def true_u(self, topol, imd_u, tmp_path): + def true_u(self, topol, imd_u, tmp_path, first_frame): u = mda.Universe( (tmp_path / topol), (tmp_path / "alanin.dcd"), ) + if not imd_u.imdsinfo.wrapped_coords: + u.trajectory.add_transformations(wrap(u.atoms, compound="atoms")) + imd_u._wrap_trajectory(u, first_frame) yield u + @pytest.fixture() + def first_frame(self): + return 0 + + +class TestIMDv3NAMD(IMDNAMDTest, IMDv3IntegrationTest): + + @pytest.fixture(params=[NAMD_CONF_V3_NST_1, NAMD_CONF_V3_NST_8]) + def inp(self, request): + return request.param + @pytest.fixture() def true_u_vel(self, topol, imd_u, tmp_path): u = mda.Universe( @@ -89,10 +106,6 @@ def true_u_force(self, topol, imd_u, tmp_path): # def match_string(self): # return "INTERACTIVE MD AWAITING CONNECTION" - @pytest.fixture() - def first_frame(self): - return 0 - # Compare coords, box, time, dt, step def test_compare_imd_to_true_traj(self, imd_u, true_u, first_frame, dt): for i in range(first_frame, len(true_u.trajectory)): @@ -146,3 +159,10 @@ def test_compare_imd_to_true_traj_forces( imd_u.trajectory[i - first_frame].forces, atol=1e-03, ) + + +class TestIMDv2NAMD(IMDNAMDTest, IMDv2IntegrationTest): + + @pytest.fixture(params=[NAMD_CONF_V2_NST_1, NAMD_CONF_V2_NST_8]) + def inp(self, request): + return request.param diff --git a/imdclient/tests/utils.py b/imdclient/tests/utils.py index 2597bca..50ed39f 100644 --- a/imdclient/tests/utils.py +++ b/imdclient/tests/utils.py @@ -10,12 +10,13 @@ def create_default_imdsinfo_v2(): return IMDSessionInfo( version=2, endianness="<", - wrapped_coords=True, + time=False, energies=True, box=False, positions=True, velocities=False, forces=False, + wrapped_coords=True, )