Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ Chronological list of authors
- Kunj Sinha
- Ayush Agarwal
- Parth Uppal
- Olivier Languin--Cattoën

External code
-------------
Expand Down
5 changes: 4 additions & 1 deletion package/CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ The rules for this file:
-------------------------------------------------------------------------------
??/??/?? IAlibay, orbeckst, marinegor, tylerjereddy, ljwoods2, marinegor,
spyke7, talagayev, tanii1125, BradyAJohnston, hejamu, jeremyleung521,
harshitgajjela-droid, kunjsinha, aygarwal, jauy123, Dreamstick9
harshitgajjela-droid, kunjsinha, aygarwal, jauy123, Dreamstick9,
ollyfutur

* 2.11.0

Expand All @@ -42,6 +43,8 @@ Fixes
DSSP by porting upstream PyDSSP 0.9.1 fix (Issue #4913)

Enhancements
* Added `select=None` in `analysis.rms.RMSD` to perform no selection on
the input `atomgroup` and `reference` (Issue #5300, PR #5296)
* MOL2Parser now reads unit cell dimensions from @<TRIPOS>CRYSIN records (Issue #3341)
* Reduces duplication of code in _apply() function (Issue #5247, PR #5294)
* Added new top-level `MDAnalysis.fetch` module (PR #4943)
Expand Down
41 changes: 31 additions & 10 deletions package/MDAnalysis/analysis/rms.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,17 +287,19 @@ 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
-------
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
-----
Expand Down Expand Up @@ -325,10 +327,16 @@ def process_selection(select):
"select dictionary must contain entries for keys "
"'mobile' and 'reference'."
) from None
elif select is None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that I keep going back and forth in my head is that 'all' and None have so much overlap I'm wondering if it's better to modify 'all' behavior to be like None (no sorting). Maybe someone more experienced should comment on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that it would be inconsistent with the usual atomgroup.select_atoms("all"), which is expected to sort… I think that conceptually None translates better the idea that no operation changes the provided groups.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just read the docs and it's clear that sorting is intended and not just a side effect. So yea, I think None probably is the best way here.

https://docs.mdanalysis.org/2.9.0/documentation_pages/selections.html

Selections always return an [AtomGroup](https://docs.mdanalysis.org/2.9.0/documentation_pages/core/groups.html#MDAnalysis.core.groups.AtomGroup) with atoms sorted according to their index in the topology (this is to ensure that there are not any duplicates, which can happen with complicated selections).

LGTM now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reflecting on this, this is how alignto handles it:

.. Warning:: The atom order for `mobile` and `reference` is *only*
preserved when `select` is either "all" or ``None``. In any other case,
a new selection will be made that will sort the resulting AtomGroup by
index and therefore destroy the correspondence between the two groups.
**It is safest not to mix ordered AtomGroups with selection strings.**

So there is a legitimate debate on whether RMSD should or should not adopt a similar behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None provides a unique functionality, i.e., leaving mobile and ref unchanged, so I see much more value in letting them do what @ollyfutur is intending here.

I'd say, the fact that align chooses "all" when selecting None is more a historical accident where None was just used as "default" and an argument could be made to change its behavior for "all". It would be more consistent if "all" behaved like a selection.

(by the way, "all" is not properly documented in https://docs.mdanalysis.org/2.10.0/documentation_pages/selections.html ... )

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I raised #5317 for documenting "all"...

select = {"reference": None, "mobile": None}
else:
raise TypeError("'select' must be either a string, 2-tuple, or dict")
select["mobile"] = asiterable(select["mobile"])
select["reference"] = asiterable(select["reference"])
raise TypeError(
"'select' must be either a string, 2-tuple, dict or None"
)
if select["mobile"] is not None:
select["mobile"] = asiterable(select["mobile"])
if select["reference"] is not None:
select["reference"] = asiterable(select["reference"])
return select


Expand Down Expand Up @@ -394,7 +402,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
Expand All @@ -405,16 +413,21 @@ 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)``

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` or `reference`
are used in their original order.

groupselections : list (optional)
A list of selections as described for `select`, with the difference
Expand Down Expand Up @@ -539,8 +552,16 @@ 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.select_atoms(*select["reference"])
if select["reference"] is not None
else self.reference
)
self.mobile_atoms = (
self.atomgroup.select_atoms(*select["mobile"])
if select["mobile"] is not None
else self.atomgroup
)

if len(self.ref_atoms) != len(self.mobile_atoms):
err = (
Expand Down
34 changes: 34 additions & 0 deletions testsuite/MDAnalysisTests/analysis/test_rms.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,40 @@ 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",
)

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()
Expand Down
Loading