From d5f4fb16c965babd33c30be1155b90e44750acbd Mon Sep 17 00:00:00 2001 From: suriyasureshok <240171.ad@rmkec.ac.in> Date: Thu, 12 Feb 2026 16:17:27 +0530 Subject: [PATCH 1/8] ENH: Add min_distance and distance_statistics functions for atom group analysis --- .gitignore | 3 + package/MDAnalysis/analysis/distances.py | 78 ++++++++++++ .../analysis/test_distances.py | 111 ++++++++++++++++++ 3 files changed, 192 insertions(+) diff --git a/.gitignore b/.gitignore index cf3cc8c87d0..e9ca72c0713 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ benchmarks/results .idea .vscode *.lock + +# virtual environments +venv/ \ No newline at end of file diff --git a/package/MDAnalysis/analysis/distances.py b/package/MDAnalysis/analysis/distances.py index 44a09c4fcbd..32c741a7d87 100644 --- a/package/MDAnalysis/analysis/distances.py +++ b/package/MDAnalysis/analysis/distances.py @@ -44,6 +44,8 @@ "contact_matrix", "dist", "between", + "min_distance", + "distance_statistics", ] import numpy as np @@ -218,3 +220,79 @@ def between(group, A, B, distance): resA = ns_group.search(A, distance) resB = ns_group.search(B, distance) return resB.intersection(resA) + + +def min_distance(A, B, box=None): + """Calculate the minimum distance between two atom groups. + + This function computes the shortest distance between any atom in group A + and any atom in group B. Useful for protein-ligand analysis, membrane-protein + distances, or identifying close contacts between molecular regions. + + Parameters + ---------- + A : AtomGroup + First atom group + B : AtomGroup + Second atom group + box : array_like, optional + Simulation cell dimensions for periodic boundary conditions in the form + ``[lx, ly, lz, alpha, beta, gamma]``. If provided, minimum image + convention is applied. + + Returns + ------- + float + Minimum distance between any atom in A and any atom in B (in Angstroms) + + .. versionadded:: 2.8.0 + """ + distances = distance_array(A.positions, B.positions, box=box) + return np.min(distances) + + +def distance_statistics(A, B, box=None): + """Calculate statistical measures of distances between two atom groups. + + Computes minimum, maximum, mean, and standard deviation of all pairwise + distances between atoms in groups A and B. Useful for characterizing + the overall separation and distribution of distances between molecular regions. + + Parameters + ---------- + A : AtomGroup + First atom group + B : AtomGroup + Second atom group + box : array_like, optional + Simulation cell dimensions for periodic boundary conditions in the form + ``[lx, ly, lz, alpha, beta, gamma]``. If provided, minimum image + convention is applied. + + Returns + ------- + dict + Dictionary containing: + + - 'min' : float + Minimum distance + - 'max' : float + Maximum distance + - 'mean' : float + Mean of all pairwise distances + - 'std' : float + Standard deviation of distances + - 'n_distances' : int + Total number of pairwise distances calculated + + .. versionadded:: 2.8.0 + """ + distances = distance_array(A.positions, B.positions, box=box) + + return { + 'min': float(np.min(distances)), + 'max': float(np.max(distances)), + 'mean': float(np.mean(distances)), + 'std': float(np.std(distances)), + 'n_distances': distances.size + } diff --git a/testsuite/MDAnalysisTests/analysis/test_distances.py b/testsuite/MDAnalysisTests/analysis/test_distances.py index 60c362d5cd6..c17ae46bffe 100644 --- a/testsuite/MDAnalysisTests/analysis/test_distances.py +++ b/testsuite/MDAnalysisTests/analysis/test_distances.py @@ -247,3 +247,114 @@ def test_between_return_type(self, dists, group, ag, ag2): returns an AtomGroup even when the returned group is empty.""" actual = MDAnalysis.analysis.distances.between(group, ag, ag2, dists) assert isinstance(actual, MDAnalysis.core.groups.AtomGroup) + +class TestMinDistance(object): + @staticmethod + @pytest.fixture() + def u(): + return MDAnalysis.Universe(GRO) + + @staticmethod + @pytest.fixture() + def ag(u): + return u.atoms[:10] + + @staticmethod + @pytest.fixture() + def ag2(u): + return u.atoms[12:33] + + @staticmethod + @pytest.fixture() + def box(): + return np.array([8, 8, 8, 90, 90, 90], dtype=np.float32) + + @pytest.fixture() + def expected(self, ag, ag2): + distance_matrix = scipy.spatial.distance.cdist(ag.positions, ag2.positions) + return np.min(distance_matrix) + + def test_min_distance_simple_case(self, ag, ag2, expected): + """Test MDAnalysis.analysis.distances.min_distance() for + a simple input case. Checks returned minimum distance + against expected value.""" + actual = MDAnalysis.analysis.distances.min_distance(ag, ag2) + assert_allclose(actual, expected) + + def test_min_distance_return_type(self, ag, ag2): + """Test that MDAnalysis.analysis.distances.min_distance() + returns a float.""" + actual = MDAnalysis.analysis.distances.min_distance(ag, ag2) + assert isinstance(actual, float) + + def test_min_distance_box(self, ag, ag2, box): + """Test that MDAnalysis.analysis.distances.min_distance() + correctly accounts for periodic boundary conditions.""" + actual = MDAnalysis.analysis.distances.min_distance(ag, ag2, box=box) + assert isinstance(actual, float) + assert actual >= 0.0 + + def test_min_distance_identical_groups(self, ag): + """Test that min_distance() returns 0 when the two AtomGroups are identical.""" + actual = MDAnalysis.analysis.distances.min_distance(ag, ag) + assert_allclose(actual, 0.0) + + def test_min_distance_single_atom_groups(self, u): + """Test that min_distance() returns the correct distance when both AtomGroups contain a single atom.""" + ag1 = u.atoms[0:1] + ag2 = u.atoms[1:2] + expected = np.linalg.norm(ag1.positions - ag2.positions) + actual = MDAnalysis.analysis.distances.min_distance(ag1, ag2) + assert_allclose(actual, expected) + +class TestDistanceStatistics(object): + @staticmethod + @pytest.fixture() + def u(): + return MDAnalysis.Universe(GRO) + + @staticmethod + @pytest.fixture() + def ag(u): + return u.atoms[:10] + + @staticmethod + @pytest.fixture() + def ag2(u): + return u.atoms[12:33] + + @staticmethod + @pytest.fixture() + def box(): + return np.array([8, 8, 8, 90, 90, 90], dtype=np.float32) + + @pytest.fixture() + def expected(self, ag, ag2): + distance_matrix = scipy.spatial.distance.cdist(ag.positions, ag2.positions) + return { + 'min': np.min(distance_matrix), + 'max': np.max(distance_matrix), + 'mean': np.mean(distance_matrix), + 'std': np.std(distance_matrix) + } + + def test_distance_statistics_simple_case(self, ag, ag2, expected): + """Test MDAnalysis.analysis.distances.distance_statistics() for + a simple input case. Checks returned distance statistics + against expected values.""" + actual = MDAnalysis.analysis.distances.distance_statistics(ag, ag2) + for key in expected: + assert_allclose(actual[key], expected[key]) + + def test_distance_statistics_return_type(self, ag, ag2): + """Test that MDAnalysis.analysis.distances.distance_statistics() + returns a dictionary.""" + actual = MDAnalysis.analysis.distances.distance_statistics(ag, ag2) + assert isinstance(actual, dict) + + def test_distance_statistics_box(self, ag, ag2, box): + """Test that MDAnalysis.analysis.distances.distance_statistics() + works correctly when a box is provided.""" + actual = MDAnalysis.analysis.distances.distance_statistics(ag, ag2, box=box) + assert isinstance(actual, dict) + assert all(key in actual for key in ['min', 'max', 'mean', 'std']) From 4df0aefee3e647c37dffa8ec24ee2ab825c69cf8 Mon Sep 17 00:00:00 2001 From: suriyasureshok <240171.ad@rmkec.ac.in> Date: Thu, 12 Feb 2026 16:24:50 +0530 Subject: [PATCH 2/8] ENH: Add Suriya Sureshkumar to the list of authors --- package/AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/package/AUTHORS b/package/AUTHORS index 973f1dd7943..38090a93346 100644 --- a/package/AUTHORS +++ b/package/AUTHORS @@ -270,6 +270,7 @@ Chronological list of authors 2026 - Mohammad Ayaan - Khushi Phougat + - Suriya Sureshkumar External code ------------- From a7fdba7b71a672b8df56605e39f5a0dbbb5b5ebd Mon Sep 17 00:00:00 2001 From: suriyasureshok <240171.ad@rmkec.ac.in> Date: Thu, 12 Feb 2026 17:04:24 +0530 Subject: [PATCH 3/8] ENH: Clean up formatting in distance_statistics function --- package/MDAnalysis/analysis/distances.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package/MDAnalysis/analysis/distances.py b/package/MDAnalysis/analysis/distances.py index 32c741a7d87..9c78176ed6c 100644 --- a/package/MDAnalysis/analysis/distances.py +++ b/package/MDAnalysis/analysis/distances.py @@ -273,7 +273,7 @@ def distance_statistics(A, B, box=None): ------- dict Dictionary containing: - + - 'min' : float Minimum distance - 'max' : float @@ -288,11 +288,11 @@ def distance_statistics(A, B, box=None): .. versionadded:: 2.8.0 """ distances = distance_array(A.positions, B.positions, box=box) - + return { - 'min': float(np.min(distances)), - 'max': float(np.max(distances)), - 'mean': float(np.mean(distances)), - 'std': float(np.std(distances)), - 'n_distances': distances.size + "min": float(np.min(distances)), + "max": float(np.max(distances)), + "mean": float(np.mean(distances)), + "std": float(np.std(distances)), + "n_distances": distances.size, } From 467fb6e9c4d680d369b51e252be749d31ef96734 Mon Sep 17 00:00:00 2001 From: suriyasureshok <240171.ad@rmkec.ac.in> Date: Fri, 13 Feb 2026 07:31:05 +0530 Subject: [PATCH 4/8] ENH: Fix GSDReader TypeError when indexing with NumPy scalar integers; add test for GSDReader numpy int indexing --- package/MDAnalysis/analysis/distances.py | 76 ------------ package/MDAnalysis/coordinates/GSD.py | 3 + .../analysis/test_distances.py | 111 ------------------ .../MDAnalysisTests/coordinates/test_gsd.py | 20 ++++ 4 files changed, 23 insertions(+), 187 deletions(-) diff --git a/package/MDAnalysis/analysis/distances.py b/package/MDAnalysis/analysis/distances.py index 9c78176ed6c..d3f5d95367b 100644 --- a/package/MDAnalysis/analysis/distances.py +++ b/package/MDAnalysis/analysis/distances.py @@ -220,79 +220,3 @@ def between(group, A, B, distance): resA = ns_group.search(A, distance) resB = ns_group.search(B, distance) return resB.intersection(resA) - - -def min_distance(A, B, box=None): - """Calculate the minimum distance between two atom groups. - - This function computes the shortest distance between any atom in group A - and any atom in group B. Useful for protein-ligand analysis, membrane-protein - distances, or identifying close contacts between molecular regions. - - Parameters - ---------- - A : AtomGroup - First atom group - B : AtomGroup - Second atom group - box : array_like, optional - Simulation cell dimensions for periodic boundary conditions in the form - ``[lx, ly, lz, alpha, beta, gamma]``. If provided, minimum image - convention is applied. - - Returns - ------- - float - Minimum distance between any atom in A and any atom in B (in Angstroms) - - .. versionadded:: 2.8.0 - """ - distances = distance_array(A.positions, B.positions, box=box) - return np.min(distances) - - -def distance_statistics(A, B, box=None): - """Calculate statistical measures of distances between two atom groups. - - Computes minimum, maximum, mean, and standard deviation of all pairwise - distances between atoms in groups A and B. Useful for characterizing - the overall separation and distribution of distances between molecular regions. - - Parameters - ---------- - A : AtomGroup - First atom group - B : AtomGroup - Second atom group - box : array_like, optional - Simulation cell dimensions for periodic boundary conditions in the form - ``[lx, ly, lz, alpha, beta, gamma]``. If provided, minimum image - convention is applied. - - Returns - ------- - dict - Dictionary containing: - - - 'min' : float - Minimum distance - - 'max' : float - Maximum distance - - 'mean' : float - Mean of all pairwise distances - - 'std' : float - Standard deviation of distances - - 'n_distances' : int - Total number of pairwise distances calculated - - .. versionadded:: 2.8.0 - """ - distances = distance_array(A.positions, B.positions, box=box) - - return { - "min": float(np.min(distances)), - "max": float(np.max(distances)), - "mean": float(np.mean(distances)), - "std": float(np.std(distances)), - "n_distances": distances.size, - } diff --git a/package/MDAnalysis/coordinates/GSD.py b/package/MDAnalysis/coordinates/GSD.py index 8212037e929..8874df048ae 100644 --- a/package/MDAnalysis/coordinates/GSD.py +++ b/package/MDAnalysis/coordinates/GSD.py @@ -128,7 +128,10 @@ def _reopen(self): self.open_trajectory() def _read_frame(self, frame): + # Convert numpy integer types to Python int for gsd compatibility + # GSD's HOOMDTrajectory only accepts Python int, not np.int64 try: + frame = int(frame) myframe = self._file[frame] except IndexError: raise IOError from None diff --git a/testsuite/MDAnalysisTests/analysis/test_distances.py b/testsuite/MDAnalysisTests/analysis/test_distances.py index c17ae46bffe..60c362d5cd6 100644 --- a/testsuite/MDAnalysisTests/analysis/test_distances.py +++ b/testsuite/MDAnalysisTests/analysis/test_distances.py @@ -247,114 +247,3 @@ def test_between_return_type(self, dists, group, ag, ag2): returns an AtomGroup even when the returned group is empty.""" actual = MDAnalysis.analysis.distances.between(group, ag, ag2, dists) assert isinstance(actual, MDAnalysis.core.groups.AtomGroup) - -class TestMinDistance(object): - @staticmethod - @pytest.fixture() - def u(): - return MDAnalysis.Universe(GRO) - - @staticmethod - @pytest.fixture() - def ag(u): - return u.atoms[:10] - - @staticmethod - @pytest.fixture() - def ag2(u): - return u.atoms[12:33] - - @staticmethod - @pytest.fixture() - def box(): - return np.array([8, 8, 8, 90, 90, 90], dtype=np.float32) - - @pytest.fixture() - def expected(self, ag, ag2): - distance_matrix = scipy.spatial.distance.cdist(ag.positions, ag2.positions) - return np.min(distance_matrix) - - def test_min_distance_simple_case(self, ag, ag2, expected): - """Test MDAnalysis.analysis.distances.min_distance() for - a simple input case. Checks returned minimum distance - against expected value.""" - actual = MDAnalysis.analysis.distances.min_distance(ag, ag2) - assert_allclose(actual, expected) - - def test_min_distance_return_type(self, ag, ag2): - """Test that MDAnalysis.analysis.distances.min_distance() - returns a float.""" - actual = MDAnalysis.analysis.distances.min_distance(ag, ag2) - assert isinstance(actual, float) - - def test_min_distance_box(self, ag, ag2, box): - """Test that MDAnalysis.analysis.distances.min_distance() - correctly accounts for periodic boundary conditions.""" - actual = MDAnalysis.analysis.distances.min_distance(ag, ag2, box=box) - assert isinstance(actual, float) - assert actual >= 0.0 - - def test_min_distance_identical_groups(self, ag): - """Test that min_distance() returns 0 when the two AtomGroups are identical.""" - actual = MDAnalysis.analysis.distances.min_distance(ag, ag) - assert_allclose(actual, 0.0) - - def test_min_distance_single_atom_groups(self, u): - """Test that min_distance() returns the correct distance when both AtomGroups contain a single atom.""" - ag1 = u.atoms[0:1] - ag2 = u.atoms[1:2] - expected = np.linalg.norm(ag1.positions - ag2.positions) - actual = MDAnalysis.analysis.distances.min_distance(ag1, ag2) - assert_allclose(actual, expected) - -class TestDistanceStatistics(object): - @staticmethod - @pytest.fixture() - def u(): - return MDAnalysis.Universe(GRO) - - @staticmethod - @pytest.fixture() - def ag(u): - return u.atoms[:10] - - @staticmethod - @pytest.fixture() - def ag2(u): - return u.atoms[12:33] - - @staticmethod - @pytest.fixture() - def box(): - return np.array([8, 8, 8, 90, 90, 90], dtype=np.float32) - - @pytest.fixture() - def expected(self, ag, ag2): - distance_matrix = scipy.spatial.distance.cdist(ag.positions, ag2.positions) - return { - 'min': np.min(distance_matrix), - 'max': np.max(distance_matrix), - 'mean': np.mean(distance_matrix), - 'std': np.std(distance_matrix) - } - - def test_distance_statistics_simple_case(self, ag, ag2, expected): - """Test MDAnalysis.analysis.distances.distance_statistics() for - a simple input case. Checks returned distance statistics - against expected values.""" - actual = MDAnalysis.analysis.distances.distance_statistics(ag, ag2) - for key in expected: - assert_allclose(actual[key], expected[key]) - - def test_distance_statistics_return_type(self, ag, ag2): - """Test that MDAnalysis.analysis.distances.distance_statistics() - returns a dictionary.""" - actual = MDAnalysis.analysis.distances.distance_statistics(ag, ag2) - assert isinstance(actual, dict) - - def test_distance_statistics_box(self, ag, ag2, box): - """Test that MDAnalysis.analysis.distances.distance_statistics() - works correctly when a box is provided.""" - actual = MDAnalysis.analysis.distances.distance_statistics(ag, ag2, box=box) - assert isinstance(actual, dict) - assert all(key in actual for key in ['min', 'max', 'mean', 'std']) diff --git a/testsuite/MDAnalysisTests/coordinates/test_gsd.py b/testsuite/MDAnalysisTests/coordinates/test_gsd.py index 102b8391df9..88ede6a9ac6 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_gsd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_gsd.py @@ -22,6 +22,7 @@ # import os +import numpy as np import pytest from numpy.testing import assert_almost_equal @@ -73,3 +74,22 @@ def test_gsd_dimensions(self, GSD_U): def test_gsd_data_step(self, GSD_U): assert GSD_U.trajectory[0].data["step"] == 0 assert GSD_U.trajectory[1].data["step"] == 500 + + def test_gsd_numpy_int_indexing(self, GSD_U): + """Test that GSDReader accepts numpy integer types (Issue #5224). + + The parallelization framework generates frame indices as np.int64, + but GSD's HOOMDTrajectory only accepts Python int. This test ensures + the reader properly converts numpy scalar integers. + """ + # Test with np.int64 (most common from numpy arrays) + ts = GSD_U.trajectory[np.int64(0)] + assert ts.frame == 0 + + # Test with negative indexing + ts = GSD_U.trajectory[np.int64(-1)] + assert ts.frame == 1 + + # Test with other numpy integer types + ts = GSD_U.trajectory[np.int32(1)] + assert ts.frame == 1 From 8a103bc78e44a4295e05309c1bc75c10f3d96e67 Mon Sep 17 00:00:00 2001 From: suriyasureshok <240171.ad@rmkec.ac.in> Date: Fri, 13 Feb 2026 07:39:55 +0530 Subject: [PATCH 5/8] ENH: Remove min_distance and distance_statistics from the public API --- package/MDAnalysis/analysis/distances.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/package/MDAnalysis/analysis/distances.py b/package/MDAnalysis/analysis/distances.py index d3f5d95367b..44a09c4fcbd 100644 --- a/package/MDAnalysis/analysis/distances.py +++ b/package/MDAnalysis/analysis/distances.py @@ -44,8 +44,6 @@ "contact_matrix", "dist", "between", - "min_distance", - "distance_statistics", ] import numpy as np From 7bb5e515557f1b8e9fd19bb8d2ccfae39ac891d7 Mon Sep 17 00:00:00 2001 From: suriyasureshok <240171.ad@rmkec.ac.in> Date: Fri, 13 Feb 2026 10:29:43 +0530 Subject: [PATCH 6/8] ENH: Remove Windows-specific test for offset lock creation in XDR reader --- testsuite/MDAnalysisTests/coordinates/test_xdr.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/testsuite/MDAnalysisTests/coordinates/test_xdr.py b/testsuite/MDAnalysisTests/coordinates/test_xdr.py index 2a2f236924e..c245420a60d 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_xdr.py +++ b/testsuite/MDAnalysisTests/coordinates/test_xdr.py @@ -1051,13 +1051,6 @@ def test_persistent_offsets_readonly(self, tmpdir, trajectory): False, ) - @pytest.mark.skipif( - sys.platform.startswith("win"), - reason="The lock file only exists when it's locked in windows", - ) - def test_offset_lock_created(self, traj): - assert os.path.exists(XDR.offsets_filename(traj, ending="lock")) - class TestXTCReader_offsets(_GromacsReader_offsets): __test__ = True From 80b3c0c2939ea363a2c7f6e52ec25b9b3fec4b65 Mon Sep 17 00:00:00 2001 From: suriyasureshok <240171.ad@rmkec.ac.in> Date: Fri, 13 Feb 2026 15:05:12 +0530 Subject: [PATCH 7/8] Fix: Restored test_offset_lock_created() --- testsuite/MDAnalysisTests/coordinates/test_xdr.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testsuite/MDAnalysisTests/coordinates/test_xdr.py b/testsuite/MDAnalysisTests/coordinates/test_xdr.py index c245420a60d..7440fda1654 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_xdr.py +++ b/testsuite/MDAnalysisTests/coordinates/test_xdr.py @@ -1051,6 +1051,12 @@ def test_persistent_offsets_readonly(self, tmpdir, trajectory): False, ) + @pytest.mark.skipif( + sys.platform.startswith("win"), + reason="The lock file only exists when it's locked in windows", + ) + def test_offset_lock_created(self, traj): + assert os.path.exists(XDR.offsets_filename(traj, ending="lock")) class TestXTCReader_offsets(_GromacsReader_offsets): __test__ = True From 650785d63c0e9ca8360b4b7e2cbf7fd402982739 Mon Sep 17 00:00:00 2001 From: suriyasureshok <240171.ad@rmkec.ac.in> Date: Fri, 13 Feb 2026 15:39:52 +0530 Subject: [PATCH 8/8] ENH: Remove virtual environment directory from .gitignore --- .gitignore | 3 --- testsuite/MDAnalysisTests/coordinates/test_gsd.py | 7 +++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index e9ca72c0713..cf3cc8c87d0 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,3 @@ benchmarks/results .idea .vscode *.lock - -# virtual environments -venv/ \ No newline at end of file diff --git a/testsuite/MDAnalysisTests/coordinates/test_gsd.py b/testsuite/MDAnalysisTests/coordinates/test_gsd.py index 88ede6a9ac6..e5478550464 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_gsd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_gsd.py @@ -93,3 +93,10 @@ def test_gsd_numpy_int_indexing(self, GSD_U): # Test with other numpy integer types ts = GSD_U.trajectory[np.int32(1)] assert ts.frame == 1 + + # Directly test _read_frame with numpy integers to ensure coverage + ts = GSD_U.trajectory._read_frame(np.int64(0)) + assert ts.frame == 0 + + ts = GSD_U.trajectory._read_frame(np.int32(1)) + assert ts.frame == 1 \ No newline at end of file