From a9ebc7e3f0a8af1f5b698ad5058c6beb68e78441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Languin-Catto=C3=ABn?= <79851513+ollyfutur@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:02:48 +0100 Subject: [PATCH 01/10] Adding no selection option RMSD analysis class now accepts `select=None` to prevent any selection on the provided `atomgroup` and `reference`. --- package/MDAnalysis/analysis/rms.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/package/MDAnalysis/analysis/rms.py b/package/MDAnalysis/analysis/rms.py index f589a597d0..bb9fb39c6f 100644 --- a/package/MDAnalysis/analysis/rms.py +++ b/package/MDAnalysis/analysis/rms.py @@ -287,11 +287,12 @@ def process_selection(select): Parameters ---------- - select : str or tuple or dict + select : str or tuple or dict or None - `str` -> Any valid string selection - `dict` -> ``{'mobile':sel1, 'reference':sel2}`` - `tuple` -> ``(sel1, sel2)`` + - ``None`` Returns ------- @@ -325,8 +326,10 @@ def process_selection(select): "select dictionary must contain entries for keys " "'mobile' and 'reference'." ) from None + elif select is None: + select = {"reference": None, "mobile": None} else: - raise TypeError("'select' must be either a string, 2-tuple, or dict") + raise TypeError("'select' must be either a string, 2-tuple, dict or None") select["mobile"] = asiterable(select["mobile"]) select["reference"] = asiterable(select["reference"]) return select @@ -394,7 +397,7 @@ def __init__( reference : AtomGroup or Universe (optional) Group of reference atoms; if ``None`` then the current frame of `atomgroup` is used. - select : str or dict or tuple (optional) + select : str or dict or tuple or None (optional) The selection to operate on; can be one of: 1. any valid selection string for @@ -409,12 +412,16 @@ def __init__( 3. a tuple ``(sel1, sel2)`` + 4. ``None`` + When using 2. or 3. with *sel1* and *sel2* then these selection strings are applied to `atomgroup` and `reference` respectively and should generate *groups of equivalent atoms*. *sel1* and *sel2* can each also be a *list of selection strings* to generate a :class:`~MDAnalysis.core.groups.AtomGroup` with defined atom order as - described under :ref:`ordered-selections-label`). + described under :ref:`ordered-selections-label`). When using ``None`` + no selection is performed and all atoms from `atomgroup` and `reference` + are used in their original order. groupselections : list (optional) A list of selections as described for `select`, with the difference @@ -539,8 +546,10 @@ def __init__( self.tol_mass = tol_mass self.ref_frame = ref_frame self.weights_groupselections = weights_groupselections - self.ref_atoms = self.reference.select_atoms(*select["reference"]) - self.mobile_atoms = self.atomgroup.select_atoms(*select["mobile"]) + self.ref_atoms = (self.reference if select["reference"] is None + else self.reference.select_atoms(*select["reference"])) + self.mobile_atoms = (self.atomgroup if select["mobile"] is None + else self.atomgroup.select_atoms(*select["mobile"])) if len(self.ref_atoms) != len(self.mobile_atoms): err = ( From 8f48321fc33593b019701c045229a4600954ffd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Languin-Catto=C3=ABn?= Date: Mon, 9 Mar 2026 18:31:15 +0100 Subject: [PATCH 02/10] RMSD no-selection test --- package/MDAnalysis/analysis/rms.py | 2 +- .../MDAnalysisTests/analysis/test_rms.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/package/MDAnalysis/analysis/rms.py b/package/MDAnalysis/analysis/rms.py index bb9fb39c6f..2e7eb9948d 100644 --- a/package/MDAnalysis/analysis/rms.py +++ b/package/MDAnalysis/analysis/rms.py @@ -327,7 +327,7 @@ def process_selection(select): "'mobile' and 'reference'." ) from None elif select is None: - select = {"reference": None, "mobile": None} + return {"reference": None, "mobile": None} else: raise TypeError("'select' must be either a string, 2-tuple, dict or None") select["mobile"] = asiterable(select["mobile"]) diff --git a/testsuite/MDAnalysisTests/analysis/test_rms.py b/testsuite/MDAnalysisTests/analysis/test_rms.py index f39baa68f1..3c29b554f2 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rms.py +++ b/testsuite/MDAnalysisTests/analysis/test_rms.py @@ -475,6 +475,28 @@ def test_rmsd_attr_warning(self, universe, client_RMSD): with pytest.warns(DeprecationWarning, match=wmsg): assert_equal(RMSD.rmsd, RMSD.results.rmsd) + def test_rmsd_no_selection(self, universe, correct_values, client_RMSD): + reference = MDAnalysis.Universe(PSF, DCD) + ref = reference.select_atoms("name CA") + ag = universe.select_atoms("name CA") + order = np.arange(len(ag)) + order[0] = 2 + order[2] = 0 + + # select=None will not sort the atomgroups + RMSD = MDAnalysis.analysis.rms.RMSD(ag[order], reference=ref, select=None) + RMSD.run(step=49, **client_RMSD) + assert not np.allclose(RMSD.results.rmsd, correct_values) + + RMSD = MDAnalysis.analysis.rms.RMSD(ag[order], reference=ref[order], select=None) + RMSD.run(step=49, **client_RMSD) + assert_almost_equal( + RMSD.results.rmsd, + correct_values, + 4, + err_msg="error: rmsd profile should match " + "between true values and calculated values", + ) class TestRMSF(object): @pytest.fixture() From 5ceb6b651ada65e29c103c7b59208f299f0e34ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Languin-Catto=C3=ABn?= Date: Mon, 9 Mar 2026 18:50:09 +0100 Subject: [PATCH 03/10] Add no-selection option to RMSD analysis --- package/CHANGELOG | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 7b75455f19..fd14131666 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -16,7 +16,7 @@ The rules for this file: ------------------------------------------------------------------------------- ??/??/?? IAlibay, orbeckst, marinegor, tylerjereddy, ljwoods2, marinegor, spyke7, talagayev, tanii1125, BradyAJohnston, hejamu, jeremyleung521, - harshitgajjela-droid, kunjsinha, aygarwal, jauy123 + harshitgajjela-droid, kunjsinha, aygarwal, jauy123, ollyfutur * 2.11.0 @@ -42,6 +42,7 @@ Fixes DSSP by porting upstream PyDSSP 0.9.1 fix (Issue #4913) Enhancements + * Allow `select=None` in `MDAnalysis.analysis.rms.RMSD` * Reduces duplication of code in _apply() function (Issue #5247, PR #5294) * Added new top-level `MDAnalysis.fetch` module (PR #4943) * Added new function `MDAnalysis.fetch.from_PDB` to download structure files from wwPDB From fe28756dfce392b63b0bf1bd69fe2795fef7e346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Languin-Catto=C3=ABn?= Date: Mon, 9 Mar 2026 18:56:35 +0100 Subject: [PATCH 04/10] Update AUTHORS --- package/AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/package/AUTHORS b/package/AUTHORS index 887210c0ae..81ada5e07c 100644 --- a/package/AUTHORS +++ b/package/AUTHORS @@ -276,6 +276,7 @@ Chronological list of authors - Kunj Sinha - Ayush Agarwal - Parth Uppal + - Olivier Languin--Cattoën External code ------------- From cfa14e2f73b63bee1e38571b3d47dd2eb4540d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Languin-Catto=C3=ABn?= Date: Mon, 9 Mar 2026 19:44:43 +0100 Subject: [PATCH 05/10] Formatting --- package/MDAnalysis/analysis/rms.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/package/MDAnalysis/analysis/rms.py b/package/MDAnalysis/analysis/rms.py index 2e7eb9948d..d75fef9366 100644 --- a/package/MDAnalysis/analysis/rms.py +++ b/package/MDAnalysis/analysis/rms.py @@ -329,7 +329,9 @@ def process_selection(select): elif select is None: return {"reference": None, "mobile": None} else: - raise TypeError("'select' must be either a string, 2-tuple, dict or None") + raise TypeError( + "'select' must be either a string, 2-tuple, dict or None" + ) select["mobile"] = asiterable(select["mobile"]) select["reference"] = asiterable(select["reference"]) return select @@ -546,10 +548,16 @@ def __init__( self.tol_mass = tol_mass self.ref_frame = ref_frame self.weights_groupselections = weights_groupselections - self.ref_atoms = (self.reference if select["reference"] is None - else self.reference.select_atoms(*select["reference"])) - self.mobile_atoms = (self.atomgroup if select["mobile"] is None - else self.atomgroup.select_atoms(*select["mobile"])) + self.ref_atoms = ( + self.reference + if select["reference"] is None + else self.reference.select_atoms(*select["reference"]) + ) + self.mobile_atoms = ( + self.atomgroup + if select["mobile"] is None + else self.atomgroup.select_atoms(*select["mobile"]) + ) if len(self.ref_atoms) != len(self.mobile_atoms): err = ( From 75754fdbfb1ec359e0dae4371666aea4f5b91d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Languin-Catto=C3=ABn?= Date: Mon, 9 Mar 2026 19:47:58 +0100 Subject: [PATCH 06/10] Formatting --- testsuite/MDAnalysisTests/analysis/test_rms.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/testsuite/MDAnalysisTests/analysis/test_rms.py b/testsuite/MDAnalysisTests/analysis/test_rms.py index 3c29b554f2..ebb3f29a2a 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rms.py +++ b/testsuite/MDAnalysisTests/analysis/test_rms.py @@ -484,11 +484,15 @@ def test_rmsd_no_selection(self, universe, correct_values, client_RMSD): order[2] = 0 # select=None will not sort the atomgroups - RMSD = MDAnalysis.analysis.rms.RMSD(ag[order], reference=ref, select=None) + RMSD = MDAnalysis.analysis.rms.RMSD( + ag[order], reference=ref, select=None + ) RMSD.run(step=49, **client_RMSD) assert not np.allclose(RMSD.results.rmsd, correct_values) - RMSD = MDAnalysis.analysis.rms.RMSD(ag[order], reference=ref[order], select=None) + RMSD = MDAnalysis.analysis.rms.RMSD( + ag[order], reference=ref[order], select=None + ) RMSD.run(step=49, **client_RMSD) assert_almost_equal( RMSD.results.rmsd, @@ -498,6 +502,7 @@ def test_rmsd_no_selection(self, universe, correct_values, client_RMSD): "between true values and calculated values", ) + class TestRMSF(object): @pytest.fixture() def universe(self): From 6ed6dc4d5683a3e100b444dbb28f110706bbb9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Languin-Catto=C3=ABn?= Date: Mon, 9 Mar 2026 20:26:11 +0100 Subject: [PATCH 07/10] Coverage --- testsuite/MDAnalysisTests/analysis/test_rms.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/testsuite/MDAnalysisTests/analysis/test_rms.py b/testsuite/MDAnalysisTests/analysis/test_rms.py index ebb3f29a2a..2141cd98bb 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rms.py +++ b/testsuite/MDAnalysisTests/analysis/test_rms.py @@ -502,6 +502,16 @@ def test_rmsd_no_selection(self, universe, correct_values, client_RMSD): "between true values and calculated values", ) + def test_rmsd_misuse_selec_raises_TypeError( + self, universe + ): + with pytest.raises(TypeError): + RMSD = MDAnalysis.analysis.rms.RMSD( + universe, + select=42, + ) + + class TestRMSF(object): @pytest.fixture() From 975b6362d059fccf566fabc25123513bfb5a7954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Languin-Catto=C3=ABn?= Date: Mon, 9 Mar 2026 20:33:34 +0100 Subject: [PATCH 08/10] Formatting --- testsuite/MDAnalysisTests/analysis/test_rms.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/testsuite/MDAnalysisTests/analysis/test_rms.py b/testsuite/MDAnalysisTests/analysis/test_rms.py index 2141cd98bb..9479c96ead 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rms.py +++ b/testsuite/MDAnalysisTests/analysis/test_rms.py @@ -502,9 +502,7 @@ def test_rmsd_no_selection(self, universe, correct_values, client_RMSD): "between true values and calculated values", ) - def test_rmsd_misuse_selec_raises_TypeError( - self, universe - ): + def test_rmsd_misuse_selec_raises_TypeError(self, universe): with pytest.raises(TypeError): RMSD = MDAnalysis.analysis.rms.RMSD( universe, @@ -512,7 +510,6 @@ def test_rmsd_misuse_selec_raises_TypeError( ) - class TestRMSF(object): @pytest.fixture() def universe(self): From 5f5760bfa76e835d7cf69d8dc36614bf569e6204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Languin-Catto=C3=ABn?= Date: Mon, 9 Mar 2026 23:12:14 +0100 Subject: [PATCH 09/10] Stylistic update --- package/MDAnalysis/analysis/rms.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package/MDAnalysis/analysis/rms.py b/package/MDAnalysis/analysis/rms.py index d75fef9366..375ede4a39 100644 --- a/package/MDAnalysis/analysis/rms.py +++ b/package/MDAnalysis/analysis/rms.py @@ -549,14 +549,14 @@ def __init__( self.ref_frame = ref_frame self.weights_groupselections = weights_groupselections self.ref_atoms = ( - self.reference - if select["reference"] is None - else self.reference.select_atoms(*select["reference"]) + self.reference.select_atoms(*select["reference"]) + if select["reference"] is not None + else self.reference ) self.mobile_atoms = ( - self.atomgroup - if select["mobile"] is None - else self.atomgroup.select_atoms(*select["mobile"]) + self.atomgroup.select_atoms(*select["mobile"]) + if select["mobile"] is not None + else self.atomgroup ) if len(self.ref_atoms) != len(self.mobile_atoms): From a6a4f27948843037303e3dfb09a369018fbd2fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Languin-Catto=C3=ABn?= Date: Thu, 12 Mar 2026 15:20:54 +0100 Subject: [PATCH 10/10] Doc and correct handling of dict of None --- package/CHANGELOG | 3 ++- package/MDAnalysis/analysis/rms.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index fd14131666..ddacbbe0de 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -42,7 +42,8 @@ Fixes DSSP by porting upstream PyDSSP 0.9.1 fix (Issue #4913) Enhancements - * Allow `select=None` in `MDAnalysis.analysis.rms.RMSD` + * Added `select=None` in `analysis.rms.RMSD` to perform no selection on + the input `atomgroup` and `reference` (Issue #5300, PR #5296) * Reduces duplication of code in _apply() function (Issue #5247, PR #5294) * Added new top-level `MDAnalysis.fetch` module (PR #4943) * Added new function `MDAnalysis.fetch.from_PDB` to download structure files from wwPDB diff --git a/package/MDAnalysis/analysis/rms.py b/package/MDAnalysis/analysis/rms.py index 375ede4a39..4b76723e7b 100644 --- a/package/MDAnalysis/analysis/rms.py +++ b/package/MDAnalysis/analysis/rms.py @@ -298,7 +298,8 @@ def process_selection(select): ------- dict selections for 'reference' and 'mobile'. Values are guarenteed to be - iterable (so that one can provide selections to retain order) + iterable (so that one can provide selections to retain order) or + ``None`` if no selection is to be performed. Notes ----- @@ -327,13 +328,15 @@ def process_selection(select): "'mobile' and 'reference'." ) from None elif select is None: - return {"reference": None, "mobile": None} + select = {"reference": None, "mobile": None} else: raise TypeError( "'select' must be either a string, 2-tuple, dict or None" ) - select["mobile"] = asiterable(select["mobile"]) - select["reference"] = asiterable(select["reference"]) + if select["mobile"] is not None: + select["mobile"] = asiterable(select["mobile"]) + if select["reference"] is not None: + select["reference"] = asiterable(select["reference"]) return select @@ -410,7 +413,8 @@ def __init__( and *sel2* are valid selection strings that are applied to `atomgroup` and `reference` respectively (the :func:`MDAnalysis.analysis.align.fasta2select` function returns such - a dictionary based on a ClustalW_ or STAMP_ sequence alignment); or + a dictionary based on a ClustalW_ or STAMP_ sequence alignment) or + ``None`` if no selection is to be performed; or 3. a tuple ``(sel1, sel2)`` @@ -422,7 +426,7 @@ def __init__( be a *list of selection strings* to generate a :class:`~MDAnalysis.core.groups.AtomGroup` with defined atom order as described under :ref:`ordered-selections-label`). When using ``None`` - no selection is performed and all atoms from `atomgroup` and `reference` + no selection is performed and all atoms from `atomgroup` or `reference` are used in their original order. groupselections : list (optional)