From 909aaf198bfb9a4a33be5f79f1c31eaa4ba4c672 Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Mon, 22 Dec 2025 15:52:16 +0530 Subject: [PATCH 01/13] Add a current frame iterator for streamed trajectories --- package/AUTHORS | 1 + package/CHANGELOG | 5 +- package/MDAnalysis/coordinates/base.py | 52 +++++++++++++++++-- .../MDAnalysisTests/coordinates/test_imd.py | 9 ++++ 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/package/AUTHORS b/package/AUTHORS index e23f2afcf2c..8db9489238a 100644 --- a/package/AUTHORS +++ b/package/AUTHORS @@ -265,6 +265,7 @@ Chronological list of authors - Raúl Lois-Cuns - Pranay Pelapkar - Shreejan Dolai + - Pardhav Maradani External code ------------- diff --git a/package/CHANGELOG b/package/CHANGELOG index 642e717b851..9752565b0d6 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -15,7 +15,7 @@ The rules for this file: ------------------------------------------------------------------------------- ??/??/?? IAlibay, orbeckst, marinegor, tylerjereddy, ljwoods2, marinegor, - spyke7, talagayev + spyke7, talagayev, PardhavMaradani * 2.11.0 @@ -32,6 +32,9 @@ Fixes Enhancements * Enables parallelization for analysis.diffusionmap.DistanceMatrix (Issue #4679, PR #4745) + * Added a current frame iterator `StreamFrameIteratorCurrent` for streamed + trajectories to enable `AnalysisBase.run` on per-frame streamed data + (Issue #5183, PR #5184) Changes * The msd.py inside analysis is changed, and ProgressBar is implemented inside diff --git a/package/MDAnalysis/coordinates/base.py b/package/MDAnalysis/coordinates/base.py index d2bfbe63d2c..da2b6c14f6f 100644 --- a/package/MDAnalysis/coordinates/base.py +++ b/package/MDAnalysis/coordinates/base.py @@ -52,6 +52,8 @@ .. autoclass:: StreamFrameIteratorSliced +.. autoclass:: StreamFrameIteratorCurrent + .. _ReadersBase: Readers @@ -1882,10 +1884,11 @@ class StreamReaderBase(ReaderBase): See Also -------- StreamFrameIteratorSliced : Iterator for stepped streaming access + StreamFrameIteratorCurrent : Iterator for current frame streaming access ReaderBase : Base class for standard trajectory readers - .. versionadded:: 2.10.0 + .. versionadded:: 2.10.0 """ def __init__(self, filename, convert_units=True, **kwargs): @@ -2040,7 +2043,7 @@ def __getitem__(self, frame): Returns ------- - FrameIteratorAll or StreamFrameIteratorSliced + FrameIteratorAll or StreamFrameIteratorSliced or StreamFrameIteratorCurrent Iterator for the requested slice. Raises @@ -2060,6 +2063,7 @@ def __getitem__(self, frame): See Also -------- StreamFrameIteratorSliced + StreamFrameIteratorCurrent """ if isinstance(frame, slice): _, _, step = self.check_slice_indices( @@ -2069,6 +2073,13 @@ def __getitem__(self, frame): return FrameIteratorAll(self) else: return StreamFrameIteratorSliced(self, step) + elif isinstance(frame, (list, np.ndarray)): + if len(frame) == 1 and frame[0] == self.trajectory.frame: + return StreamFrameIteratorCurrent(self) + else: + raise ValueError( + "Streamed trajectories must have single current frame value" + ) else: raise TypeError( "Streamed trajectories must be an indexed using a slice" @@ -2310,4 +2321,39 @@ def step(self): Step size for iteration. Always a positive integer greater than 0. """ - return self._step \ No newline at end of file + return self._step + + +class StreamFrameIteratorCurrent(FrameIteratorBase): + """Iterator for current frame access in a streamed trajectory. + + Created when an array with a single current frame value is passed. + + Parameters + ---------- + trajectory : StreamReaderBase + The streaming trajectory reader to iterate over. Must be a + stream-based reader that supports continuous data reading. + + See Also + -------- + StreamReaderBase + FrameIteratorBase + + .. versionadded:: 2.11.0 + """ + + def __init__(self, trajectory): + super(StreamFrameIteratorCurrent, self).__init__(trajectory) + + def __len__(self): + return 1 + + def __iter__(self): + yield self.trajectory._read_frame_with_aux(self.trajectory.frame) + + def __next__(self): + raise StopIteration from None + + def __getitem__(self, frame): + raise RuntimeError("Current frame iterator does not support indexing") diff --git a/testsuite/MDAnalysisTests/coordinates/test_imd.py b/testsuite/MDAnalysisTests/coordinates/test_imd.py index 2fed2f761fd..07b6ddf9bc5 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_imd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_imd.py @@ -507,6 +507,15 @@ def test_step_property(self, reader): sliced_reader_step5 = reader[::5] assert sliced_reader_step5.step == 5 + def test_iterate_current_frame_raises_error(self, reader): + with pytest.raises(ValueError, match="must have single current frame value"): + for ts in reader[[1]]: + pass + + def test_iterate_current_frame(self, reader): + for ts in reader[[reader.frame]]: + assert ts.frame == reader.frame + @pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not installed") def test_n_atoms_not_specified(universe, imdsinfo): From 899541324513dfd90ee24f933b3f0bef4d0df11f Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Mon, 22 Dec 2025 16:28:50 +0530 Subject: [PATCH 02/13] * Fix test formatting --- testsuite/MDAnalysisTests/coordinates/test_imd.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testsuite/MDAnalysisTests/coordinates/test_imd.py b/testsuite/MDAnalysisTests/coordinates/test_imd.py index 07b6ddf9bc5..21a27f6ca5c 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_imd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_imd.py @@ -508,7 +508,10 @@ def test_step_property(self, reader): assert sliced_reader_step5.step == 5 def test_iterate_current_frame_raises_error(self, reader): - with pytest.raises(ValueError, match="must have single current frame value"): + with pytest.raises( + ValueError, + match="must have single current frame value", + ): for ts in reader[[1]]: pass From 4def11fa390ebbc56aaef6052eea1bb55c778ad7 Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Mon, 22 Dec 2025 16:41:58 +0530 Subject: [PATCH 03/13] * Add tests to fix coverage errors --- testsuite/MDAnalysisTests/coordinates/test_imd.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testsuite/MDAnalysisTests/coordinates/test_imd.py b/testsuite/MDAnalysisTests/coordinates/test_imd.py index 21a27f6ca5c..1cc642e1f02 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_imd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_imd.py @@ -514,8 +514,17 @@ def test_iterate_current_frame_raises_error(self, reader): ): for ts in reader[[1]]: pass + ts = reader[[0]] + with pytest.raises(StopIteration): + next(ts) + with pytest.raises( + RuntimeError, + match="Current frame iterator does not support indexing", + ): + ts[0] def test_iterate_current_frame(self, reader): + assert len(reader[[reader.frame]]) == 1 for ts in reader[[reader.frame]]: assert ts.frame == reader.frame From 04f60c049463a8943cd97dd49a3f7cb293a6966f Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Tue, 23 Dec 2025 08:57:47 +0530 Subject: [PATCH 04/13] * Move CHANGELOG entry to the top --- package/CHANGELOG | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 9752565b0d6..ab641744021 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -30,11 +30,11 @@ Fixes DSSP by porting upstream PyDSSP 0.9.1 fix (Issue #4913) Enhancements - * Enables parallelization for analysis.diffusionmap.DistanceMatrix - (Issue #4679, PR #4745) * Added a current frame iterator `StreamFrameIteratorCurrent` for streamed trajectories to enable `AnalysisBase.run` on per-frame streamed data (Issue #5183, PR #5184) + * Enables parallelization for analysis.diffusionmap.DistanceMatrix + (Issue #4679, PR #4745) Changes * The msd.py inside analysis is changed, and ProgressBar is implemented inside From 4948f40249a835227248492b3dc3fcc52382277a Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Fri, 26 Dec 2025 23:25:50 +0530 Subject: [PATCH 05/13] * Allow current frame access for streamed trajectories * This enables analysis classes to use current frame as reference in `__init__` --- package/MDAnalysis/coordinates/base.py | 9 ++++++++- testsuite/MDAnalysisTests/coordinates/test_imd.py | 8 ++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/package/MDAnalysis/coordinates/base.py b/package/MDAnalysis/coordinates/base.py index da2b6c14f6f..58261cd1a0a 100644 --- a/package/MDAnalysis/coordinates/base.py +++ b/package/MDAnalysis/coordinates/base.py @@ -2065,7 +2065,14 @@ def __getitem__(self, frame): StreamFrameIteratorSliced StreamFrameIteratorCurrent """ - if isinstance(frame, slice): + if isinstance(frame, numbers.Integral): + if frame == self.trajectory.frame: + return self._read_frame_with_aux(frame) + else: + raise ValueError( + "Streamed trajectories must specify current frame value" + ) + elif isinstance(frame, slice): _, _, step = self.check_slice_indices( frame.start, frame.stop, frame.step ) diff --git a/testsuite/MDAnalysisTests/coordinates/test_imd.py b/testsuite/MDAnalysisTests/coordinates/test_imd.py index 1cc642e1f02..9b7e6ae0573 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_imd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_imd.py @@ -469,8 +469,12 @@ def test_iterate_twice_fi_all_raises_error(self, reader): pass def test_index_stream_raises_error(self, reader): - with pytest.raises(TypeError, match="Streamed trajectories must be"): - reader[0] + reader[0] + with pytest.raises( + ValueError, + match="Streamed trajectories must specify current frame value", + ): + reader[1] def test_iterate_backwards_raises_error(self, reader): with pytest.raises(ValueError, match="Cannot go backwards"): From 400d6a6c5c57b35ac7ddcd5934de130c190e38e0 Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Sat, 27 Dec 2025 00:34:07 +0530 Subject: [PATCH 06/13] * Add partial coverage test --- testsuite/MDAnalysisTests/coordinates/test_imd.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testsuite/MDAnalysisTests/coordinates/test_imd.py b/testsuite/MDAnalysisTests/coordinates/test_imd.py index 9b7e6ae0573..badfb4abef6 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_imd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_imd.py @@ -531,6 +531,7 @@ def test_iterate_current_frame(self, reader): assert len(reader[[reader.frame]]) == 1 for ts in reader[[reader.frame]]: assert ts.frame == reader.frame + reader[np.array([reader.frame])] @pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not installed") From 2afa13779f551ee307ad5a8840ab5e68a18e05c8 Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Sat, 27 Dec 2025 19:41:35 +0530 Subject: [PATCH 07/13] * Ensure timestep doesn't change * Added more tests --- package/MDAnalysis/coordinates/IMD.py | 3 +++ .../MDAnalysisTests/coordinates/test_imd.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/package/MDAnalysis/coordinates/IMD.py b/package/MDAnalysis/coordinates/IMD.py index 4945d43adfc..16b6bf93dc0 100644 --- a/package/MDAnalysis/coordinates/IMD.py +++ b/package/MDAnalysis/coordinates/IMD.py @@ -286,6 +286,9 @@ def __init__( raise RuntimeError(f"IMDReader: Read error: {e}") from e def _read_frame(self, frame): + if frame == self._frame: + logger.debug("IMDReader: Using current frame %d", self._frame) + return self.ts imdf = self._imdclient.get_imdframe() diff --git a/testsuite/MDAnalysisTests/coordinates/test_imd.py b/testsuite/MDAnalysisTests/coordinates/test_imd.py index badfb4abef6..ad0587d621e 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_imd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_imd.py @@ -528,10 +528,27 @@ def test_iterate_current_frame_raises_error(self, reader): ts[0] def test_iterate_current_frame(self, reader): + cts = reader.ts + # test iterator length assert len(reader[[reader.frame]]) == 1 + # test list iterator for ts in reader[[reader.frame]]: + assert ts == cts assert ts.frame == reader.frame + # test np.ndarray iterator reader[np.array([reader.frame])] + # test same timestep + assert reader[reader.frame] == cts + assert reader[reader.frame] == reader[reader.frame] + # should be able to iterate all 5 frames in reader + # due to server.send_frames(1, 5) in reader setup + for i in range(5): + ts = reader[i] + if i < 4: + reader.next() + else: + with pytest.raises(StopIteration): + reader.next() @pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not installed") From 228e17202693a07a8e2894d1fe3766acd22eaf4a Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Sun, 28 Dec 2025 17:38:37 +0530 Subject: [PATCH 08/13] * Add tests for AnalysisBase-based classes --- .../MDAnalysisTests/coordinates/test_imd.py | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) diff --git a/testsuite/MDAnalysisTests/coordinates/test_imd.py b/testsuite/MDAnalysisTests/coordinates/test_imd.py index ad0587d621e..6091c6e4040 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_imd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_imd.py @@ -551,6 +551,256 @@ def test_iterate_current_frame(self, reader): reader.next() +@pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not installed") +class TestAnalysisClasses: + """ + Tests for AnalysisBase-based classes + + Following classes currently do not work: + align.AlignTraj + align.AverageStructure + diffusionmap.DiffusionMap + gnm.GNMAnalysis + gnm.closeContactGNMAnalysis + pca.PCA + polymer.PersistenceLength + rms.RMSD + hydrogenbonds.HydrogenBondAutoCorrel + + """ + + @pytest.fixture + def create_imd_universe(self): + server = None + + def _create_imd_universe( + topo, + traj, + velocities=True, + forces=True, + box=True, + frames=5, + ): + nonlocal server + u = mda.Universe(topo, traj) + server = InThreadIMDServer(u.trajectory) + info = create_default_imdsinfo_v3() + info.velocities = velocities + info.forces = forces + info.box = box + server.set_imdsessioninfo(info) + server.handshake_sequence("localhost", first_frame=True) + u_imd = mda.Universe( + topo, + f"imd://localhost:{server.port}", + n_atoms=u.trajectory.n_atoms, + ) + server.send_frames(1, frames) + return u_imd + + yield _create_imd_universe + server.cleanup() + + @pytest.fixture + def u1(self, create_imd_universe): + from MDAnalysisTests.datafiles import TPR, XTC + + return create_imd_universe( + topo=TPR, + traj=XTC, + velocities=False, + forces=False, + ) + + @pytest.fixture + def u2(self, create_imd_universe): + from MDAnalysisTests.datafiles import PSF_TRICLINIC, DCD_TRICLINIC + + return create_imd_universe( + topo=PSF_TRICLINIC, + traj=DCD_TRICLINIC, + velocities=False, + forces=False, + ) + + @pytest.fixture + def u3(self, create_imd_universe): + from MDAnalysisTests.datafiles import RNA_PSF, RNA_PDB + + return create_imd_universe( + topo=RNA_PSF, + traj=RNA_PDB, + velocities=False, + forces=False, + box=False, + frames=1, + ) + + def test_atomicdistances(self, u1): + from MDAnalysis.analysis.atomicdistances import AtomicDistances + + ag1 = u1.atoms[10:20] + ag2 = u1.atoms[70:80] + ad = AtomicDistances(ag1, ag2) + for i in range(3): + ad.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_bat(self, u1): + from MDAnalysis.analysis.bat import BAT + + selected_residues = u1.select_atoms("resid 1:10") + bat = BAT(selected_residues) + for i in range(3): + bat.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_contacts(self, u1): + from MDAnalysis.analysis.contacts import Contacts + + sel_basic = "(resname ARG LYS) and (name NH* NZ)" + sel_acidic = "(resname ASP GLU) and (name OE* OD*)" + acidic = u1.select_atoms(sel_acidic) + basic = u1.select_atoms(sel_basic) + ca1 = Contacts( + u1, + select=(sel_acidic, sel_basic), + refgroup=(acidic, basic), + radius=6.0, + ) + for i in range(3): + ca1.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_density(self, u1): + from MDAnalysis.analysis.density import DensityAnalysis + + ow = u1.select_atoms("protein") + da = DensityAnalysis(ow, delta=1.0) + for i in range(3): + da.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_dielectric(self, u2): + from MDAnalysis.analysis.dielectric import DielectricConstant + + diel = DielectricConstant(u2.atoms) + for i in range(3): + diel.run(frames=[u2.trajectory.frame]) + u2.trajectory.next() + + def test_diffusionmap(self, u1): + import MDAnalysis.analysis.diffusionmap as diffusionmap + + dm = diffusionmap.DistanceMatrix(u1, select="backbone") + for i in range(3): + dm.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_dihedrals(self, u1): + from MDAnalysis.analysis.dihedrals import Dihedral, Janin, Ramachandran + + ags = [res.phi_selection() for res in u1.residues[4:9]] + d = Dihedral(ags) + rama = Ramachandran(u1.select_atoms("protein")) + janin = Janin(u1.select_atoms("protein")) + for i in range(3): + d.run(frames=[u1.trajectory.frame]) + rama.run(frames=[u1.trajectory.frame]) + janin.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_helix_analysis(self, u1): + from MDAnalysis.analysis import helix_analysis as hel + + ha = hel.HELANAL( + u1.select_atoms("name CA"), + select="resnum 1-9", + flatten_single_helix=True, + ) + for i in range(3): + ha.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_lineardensity(self, u1): + from MDAnalysis.analysis.lineardensity import LinearDensity + + ld = LinearDensity(u1.select_atoms("all"), binsize=5) + for i in range(3): + ld.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_msd(self, u1): + from MDAnalysis.analysis import msd + + m = msd.EinsteinMSD(u1, "all", non_linear=True) + for i in range(3): + m.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_nucleicacids(self, u3): + from MDAnalysis.analysis import nucleicacids + from MDAnalysis.core.groups import ResidueGroup + + strand = u3.select_atoms("resid 1-100") + strand1 = ResidueGroup([strand.residues[0], strand.residues[21]]) + strand2 = ResidueGroup([strand.residues[2], strand.residues[22]]) + wcd = nucleicacids.WatsonCrickDist(strand1, strand2) + wcd.run(frames=[u3.trajectory.frame]) + strand1 = ResidueGroup([strand.residues[2], strand.residues[19]]) + strand2 = ResidueGroup([strand.residues[16], strand.residues[4]]) + mi = nucleicacids.MinorPairDist(strand1, strand2) + mi.run(frames=[u3.trajectory.frame]) + strand1 = ResidueGroup([strand.residues[1], strand.residues[4]]) + strand2 = ResidueGroup([strand.residues[11], strand.residues[8]]) + ma = nucleicacids.MajorPairDist(strand1, strand2) + ma.run(frames=[u3.trajectory.frame]) + + def test_rdf(self, u1): + from MDAnalysis.analysis import rdf + + ag1 = u1.select_atoms("resid 1:10") + ag2 = u1.select_atoms("resid 30:40") + r = rdf.InterRDF(ag1, ag2) + r_s = rdf.InterRDF_s(u1, [[ag1, ag1], [ag2, ag2]]) + for i in range(3): + r.run(frames=[u1.trajectory.frame]) + r_s.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_rmsf(self, u1): + from MDAnalysis.analysis.rms import RMSF + + rmsf = RMSF(u1.select_atoms("name CA")) + for i in range(3): + rmsf.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_dssp(self, u1): + from MDAnalysis.analysis.dssp import DSSP + + dssp = DSSP(u1) + for i in range(3): + dssp.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_hba(self, u1): + from MDAnalysis.analysis.hydrogenbonds import hbond_analysis + + hba = hbond_analysis.HydrogenBondAnalysis(universe=u1) + for i in range(2): + hba.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + def test_wba(self, u1): + from MDAnalysis.analysis.hydrogenbonds import WaterBridgeAnalysis + + wba = WaterBridgeAnalysis(u1, "resname ARG", "resname ASP") + for i in range(2): + wba.run(frames=[u1.trajectory.frame]) + u1.trajectory.next() + + @pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not installed") def test_n_atoms_not_specified(universe, imdsinfo): server = InThreadIMDServer(universe.trajectory) From 628ec20c8e8f5c0a575ff0a54740b4aec3d6a544 Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Sun, 28 Dec 2025 19:03:59 +0530 Subject: [PATCH 09/13] * Don't reapply transformations for current frame --- package/MDAnalysis/coordinates/base.py | 4 ++-- testsuite/MDAnalysisTests/coordinates/test_imd.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package/MDAnalysis/coordinates/base.py b/package/MDAnalysis/coordinates/base.py index 58261cd1a0a..691784f060e 100644 --- a/package/MDAnalysis/coordinates/base.py +++ b/package/MDAnalysis/coordinates/base.py @@ -2067,7 +2067,7 @@ def __getitem__(self, frame): """ if isinstance(frame, numbers.Integral): if frame == self.trajectory.frame: - return self._read_frame_with_aux(frame) + return self._read_frame(frame) else: raise ValueError( "Streamed trajectories must specify current frame value" @@ -2357,7 +2357,7 @@ def __len__(self): return 1 def __iter__(self): - yield self.trajectory._read_frame_with_aux(self.trajectory.frame) + yield self.trajectory._read_frame(self.trajectory.frame) def __next__(self): raise StopIteration from None diff --git a/testsuite/MDAnalysisTests/coordinates/test_imd.py b/testsuite/MDAnalysisTests/coordinates/test_imd.py index 6091c6e4040..aa8747db921 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_imd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_imd.py @@ -550,6 +550,14 @@ def test_iterate_current_frame(self, reader): with pytest.raises(StopIteration): reader.next() + def test_iterate_current_frame_no_transformations(self, reader): + reader.add_transformations( + translate([1, 1, 1]), translate([0, 0, 0.33]) + ) + p1 = reader[reader.frame].positions.copy() + p2 = reader[reader.frame].positions + assert_allclose(p1, p2) + @pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not installed") class TestAnalysisClasses: From 8fca0f88a8820f74265e10c2afd5d542206ff865 Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Sun, 28 Dec 2025 20:09:34 +0530 Subject: [PATCH 10/13] * Use simpler universe for hbond and water bridge analysis --- .../MDAnalysisTests/coordinates/test_imd.py | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/testsuite/MDAnalysisTests/coordinates/test_imd.py b/testsuite/MDAnalysisTests/coordinates/test_imd.py index aa8747db921..7edc903d214 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_imd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_imd.py @@ -644,6 +644,17 @@ def u3(self, create_imd_universe): frames=1, ) + @pytest.fixture + def u4(self, create_imd_universe): + from MDAnalysisTests.datafiles import waterPSF, waterDCD + + return create_imd_universe( + topo=waterPSF, + traj=waterDCD, + velocities=False, + forces=False, + ) + def test_atomicdistances(self, u1): from MDAnalysis.analysis.atomicdistances import AtomicDistances @@ -792,21 +803,21 @@ def test_dssp(self, u1): dssp.run(frames=[u1.trajectory.frame]) u1.trajectory.next() - def test_hba(self, u1): + def test_hba(self, u4): from MDAnalysis.analysis.hydrogenbonds import hbond_analysis - hba = hbond_analysis.HydrogenBondAnalysis(universe=u1) - for i in range(2): - hba.run(frames=[u1.trajectory.frame]) - u1.trajectory.next() + hba = hbond_analysis.HydrogenBondAnalysis(universe=u4) + for i in range(3): + hba.run(frames=[u4.trajectory.frame]) + u4.trajectory.next() - def test_wba(self, u1): + def test_wba(self, u4): from MDAnalysis.analysis.hydrogenbonds import WaterBridgeAnalysis - wba = WaterBridgeAnalysis(u1, "resname ARG", "resname ASP") - for i in range(2): - wba.run(frames=[u1.trajectory.frame]) - u1.trajectory.next() + wba = WaterBridgeAnalysis(u4, "resid 1:10", "resid 10:20") + for i in range(3): + wba.run(frames=[u4.trajectory.frame]) + u4.trajectory.next() @pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not installed") From 20c3a5399dda9d87718a16fbe0022843a1edd70a Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Sun, 28 Dec 2025 21:36:27 +0530 Subject: [PATCH 11/13] * Empty commit to re-run tests From dedb1ea18c9bd376d18658e79e317c713d2a073d Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Mon, 29 Dec 2025 09:28:31 +0530 Subject: [PATCH 12/13] * Add tests for stream continuity during iteration --- package/MDAnalysis/coordinates/base.py | 4 +++- .../MDAnalysisTests/coordinates/test_imd.py | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/package/MDAnalysis/coordinates/base.py b/package/MDAnalysis/coordinates/base.py index 691784f060e..eeb89521dd6 100644 --- a/package/MDAnalysis/coordinates/base.py +++ b/package/MDAnalysis/coordinates/base.py @@ -2008,7 +2008,9 @@ def _reopen(self): raise RuntimeError( "{}: Cannot reopen stream".format(self.__class__.__name__) ) - self._frame = -1 + if self._frame == 0: + # only reset when stream hasn't been iterated using next + self._frame = -1 self._reopen_called = True def timeseries(self, **kwargs): diff --git a/testsuite/MDAnalysisTests/coordinates/test_imd.py b/testsuite/MDAnalysisTests/coordinates/test_imd.py index 7edc903d214..1feb1d1498e 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_imd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_imd.py @@ -558,6 +558,27 @@ def test_iterate_current_frame_no_transformations(self, reader): p2 = reader[reader.frame].positions assert_allclose(p1, p2) + def test_iterate_continuity_1(self, reader): + step = -1 + for ts in reader: + step += 1 + assert ts.data["step"] == step + if step == 4: + break + + def test_iterate_continuity_2(self, reader): + ts = reader[0] + assert ts.data["step"] == 0 + reader.next() + ts = reader[1] + assert ts.data["step"] == 1 + step = 1 + for ts in reader: + step += 1 + assert ts.data["step"] == step + if step == 4: + break + @pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not installed") class TestAnalysisClasses: From 833875aeb1c5c0731522558511344181c4aabae3 Mon Sep 17 00:00:00 2001 From: Pardhav Maradani Date: Wed, 31 Dec 2025 12:07:42 +0530 Subject: [PATCH 13/13] * Refactor tests a bit --- .../MDAnalysisTests/coordinates/test_imd.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/testsuite/MDAnalysisTests/coordinates/test_imd.py b/testsuite/MDAnalysisTests/coordinates/test_imd.py index 1feb1d1498e..3d8b41e66bf 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_imd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_imd.py @@ -527,35 +527,38 @@ def test_iterate_current_frame_raises_error(self, reader): ): ts[0] - def test_iterate_current_frame(self, reader): + @pytest.mark.parametrize("iter_type", [list, np.array]) + def test_iterate_current_frame(self, reader, iter_type): cts = reader.ts # test iterator length - assert len(reader[[reader.frame]]) == 1 + assert len(reader[iter_type([reader.frame])]) == 1 # test list iterator - for ts in reader[[reader.frame]]: + for ts in reader[iter_type([reader.frame])]: assert ts == cts assert ts.frame == reader.frame - # test np.ndarray iterator - reader[np.array([reader.frame])] + + def test_current_frame(self, reader): + cts = reader.ts # test same timestep assert reader[reader.frame] == cts assert reader[reader.frame] == reader[reader.frame] # should be able to iterate all 5 frames in reader # due to server.send_frames(1, 5) in reader setup for i in range(5): - ts = reader[i] + reader[i] if i < 4: reader.next() else: with pytest.raises(StopIteration): reader.next() - def test_iterate_current_frame_no_transformations(self, reader): + def test_current_frame_transformations(self, reader): reader.add_transformations( translate([1, 1, 1]), translate([0, 0, 0.33]) ) p1 = reader[reader.frame].positions.copy() p2 = reader[reader.frame].positions + # test transformations not repeated assert_allclose(p1, p2) def test_iterate_continuity_1(self, reader):