Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gyration_moments method / allow shape parameters to yield per residue quantities #3905

Merged
merged 14 commits into from
Mar 15, 2023
6 changes: 5 additions & 1 deletion package/CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ The rules for this file:
------------------------------------------------------------------------------

??/??/?? IAlibay, pgbarletta, mglagolev, hmacdope, manuel.nuno.melo, chrispfae,
ooprathamm, MeetB7, BFedder, v-parmar, MoSchaeffler
ooprathamm, MeetB7, BFedder, v-parmar, MoSchaeffler, jaclark5
* 2.5.0

Fixes
* Allows shape_parameter and asphericity to yield per residue quantities
(Issue #3002, PR #3905)
* Fix tests should use results.rmsf to avoid DeprecationWarning (Issue #3695)
* Fix EDRReader failing when parsing single-frame EDR files (Issue #3999)
* Fix element parsing from PSF files tests read via Parmed (Issue #4015)
Expand All @@ -28,6 +30,8 @@ Fixes
(Issue #3336)

Enhancements
* Added AtomGroup TopologyAttr to calculate gyration moments (Issue #3904,
PR #3905)
* Add distopia distance calculation library bindings as a selectable backend
for `calc_bonds` in `MDA.lib.distances`. (Issue #3783, PR #3914)
* AuxReaders are now pickle-able and copy-able (Issue #1785, PR #3887)
Expand Down
152 changes: 113 additions & 39 deletions package/MDAnalysis/core/topologyattrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1729,7 +1729,99 @@ def radius_of_gyration(group, wrap=False, **kwargs):
@warn_if_not_unique
@_pbc_to_wrap
@check_atomgroup_not_empty
def shape_parameter(group, wrap=False):
def gyration_moments(group, wrap=False, unwrap=None, compound='group'):
hmacdope marked this conversation as resolved.
Show resolved Hide resolved
jaclark5 marked this conversation as resolved.
Show resolved Hide resolved
r"""Moments of the gyration tensor.

The moments are defined as the eigenvalues of the gyration
tensor.

.. math::

\mathsf{T} = \frac{1}{N} \sum_{i=1}^{N} (\mathsf{r_{i}} -
\mathsf{r_{COM}})(\mathsf{r_{i}} - \mathsf{r_{COM}})

Where :math:`r_{COM}` is the center of mass.
jaclark5 marked this conversation as resolved.
Show resolved Hide resolved

See [Dima2004a]_ for background information.

Parameters
----------
wrap : bool, optional
If ``True``, move all atoms within the primary unit cell before
calculation. [``False``]
unwrap : bool, optional
If ``True``, compounds will be unwrapped before computing their centers.
compound : {'group', 'segments', 'residues', 'molecules', 'fragments'}, optional
Which type of component to keep together during unwrapping.

Returns
-------
principle_moments_of_gyration : numpy.ndarray
Gyration vector(s) of (compounds of) the group in :math:`Å^2`.
If `compound` was set to ``'group'``, the output will be a single
value. Otherwise, the output will be a 1d array of shape ``(n,3)``
jaclark5 marked this conversation as resolved.
Show resolved Hide resolved
where ``n`` is the number of compounds.

jaclark5 marked this conversation as resolved.
Show resolved Hide resolved
.. versionadded:: 2.5.0
"""

def __gyration(recenteredpos, masses):
jaclark5 marked this conversation as resolved.
Show resolved Hide resolved
if len(masses.shape) > 1:
masses = np.squeeze(masses)
tensor = np.einsum( "ki,kj->ij",
recenteredpos,
np.einsum("ij,i->ij", recenteredpos, masses),
)
return np.linalg.eigvalsh(tensor/np.sum(masses))

atomgroup = group.atoms
masses = atomgroup.masses

com = atomgroup.center_of_mass(
wrap=wrap, unwrap=unwrap, compound=compound)

if compound == 'group':
if wrap:
recenteredpos = (atomgroup.pack_into_box(inplace=False) - com)
elif unwrap:
recenteredpos = (atomgroup.unwrap(inplace=False,
compound=compound,
reference=None,
) - com)
else:
recenteredpos = (atomgroup.positions - com)
eig_vals = __gyration(recenteredpos, masses)
else:
(atom_masks,
compound_masks,
n_compounds) = atomgroup._split_by_compound_indices(compound)

if unwrap:
coords = atomgroup.unwrap(
compound=compound,
reference=None,
inplace=False
)
else:
coords = atomgroup.positions

eig_vals = np.empty((n_compounds,3), dtype=np.float64)
jaclark5 marked this conversation as resolved.
Show resolved Hide resolved
for compound_mask, atom_mask in zip(compound_masks, atom_masks):
eig_vals[compound_mask,:] = [__gyration(
jaclark5 marked this conversation as resolved.
Show resolved Hide resolved
coords[mask] - com[compound_mask][i],
masses[mask][:,None]
jaclark5 marked this conversation as resolved.
Show resolved Hide resolved
) for i,mask in enumerate(atom_mask)]
jaclark5 marked this conversation as resolved.
Show resolved Hide resolved

return eig_vals

transplants[GroupBase].append(
('gyration_moments', gyration_moments))


@warn_if_not_unique
@_pbc_to_wrap
@check_atomgroup_not_empty
def shape_parameter(group, wrap=False, unwrap=None, compound='group'):
jaclark5 marked this conversation as resolved.
Show resolved Hide resolved
"""Shape parameter.

See [Dima2004a]_ for background information.
Expand All @@ -1739,6 +1831,10 @@ def shape_parameter(group, wrap=False):
wrap : bool, optional
If ``True``, move all atoms within the primary unit cell before
calculation. [``False``]
unwrap : bool, optional
If ``True``, compounds will be unwrapped before computing their centers.
compound : {'group', 'segments', 'residues', 'molecules', 'fragments'}, optional
Which type of component to keep together during unwrapping.


.. versionadded:: 0.7.7
Expand All @@ -1748,24 +1844,17 @@ def shape_parameter(group, wrap=False):
Renamed `pbc` kwarg to `wrap`. `pbc` is still accepted but
is deprecated and will be removed in version 3.0.
Superfluous kwargs were removed.
.. versionchanged:: 2.5.0
Added use of gyration_moments for per residue quantities
orbeckst marked this conversation as resolved.
Show resolved Hide resolved
"""
atomgroup = group.atoms
masses = atomgroup.masses

com = atomgroup.center_of_mass(wrap=wrap)
if wrap:
recenteredpos = atomgroup.pack_into_box(inplace=False) - com
eig_vals = atomgroup.gyration_moments(wrap=wrap, unwrap=unwrap, compound=compound)
if len(eig_vals.shape) > 1:
shape = 27.0 * np.prod(eig_vals - np.mean(eig_vals, axis=1), axis=1
) / np.power(np.sum(eig_vals, axis=1), 3)
else:
recenteredpos = atomgroup.positions - com
tensor = np.zeros((3, 3))

for x in range(recenteredpos.shape[0]):
tensor += masses[x] * np.outer(recenteredpos[x, :],
recenteredpos[x, :])
tensor /= atomgroup.total_mass()
eig_vals = np.linalg.eigvalsh(tensor)
shape = 27.0 * np.prod(eig_vals - np.mean(eig_vals)
) / np.power(np.sum(eig_vals), 3)
shape = 27.0 * np.prod(eig_vals - np.mean(eig_vals)
) / np.power(np.sum(eig_vals), 3)

return shape

Expand Down Expand Up @@ -1800,32 +1889,17 @@ def asphericity(group, wrap=False, unwrap=None, compound='group'):
.. versionchanged:: 2.1.0
Renamed `pbc` kwarg to `wrap`. `pbc` is still accepted but
is deprecated and will be removed in version 3.0.
.. versionchanged:: 2.5.0
Added use of gyration_moments for per residue quantities
Copy link
Member

Choose a reason for hiding this comment

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

Is this important for the user to know how the quantities are computed? I'd assume the more important message is what you have in the CHANGELOG: since 2.5.0 you can calculate asphericity for any compound.

Copy link
Contributor Author

@jaclark5 jaclark5 Mar 2, 2023

Choose a reason for hiding this comment

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

That's what I was going for with "for per residue quantities" but hopeful the change is more clear.

"""
atomgroup = group.atoms
masses = atomgroup.masses

com = atomgroup.center_of_mass(
wrap=wrap, unwrap=unwrap, compound=compound)
if compound != 'group':
com = (com * group.masses[:, None]
).sum(axis=0) / group.masses.sum()

if wrap:
recenteredpos = (atomgroup.pack_into_box(inplace=False) - com)
elif unwrap:
recenteredpos = (atomgroup.unwrap(inplace=False) - com)
eig_vals = atomgroup.gyration_moments(wrap=wrap, unwrap=unwrap, compound=compound)
if len(eig_vals.shape) > 1:
shape = (3.0 / 2.0) * (np.sum((eig_vals - np.mean(eig_vals, axis=1))**2, axis=1) /
np.sum(eig_vals, axis=1)**2)
else:
recenteredpos = (atomgroup.positions - com)

tensor = np.zeros((3, 3))
for x in range(recenteredpos.shape[0]):
tensor += masses[x] * np.outer(recenteredpos[x],
recenteredpos[x])

tensor /= atomgroup.total_mass()
eig_vals = np.linalg.eigvalsh(tensor)
shape = (3.0 / 2.0) * (np.sum((eig_vals - np.mean(eig_vals))**2) /
np.sum(eig_vals)**2)
shape = (3.0 / 2.0) * (np.sum((eig_vals - np.mean(eig_vals))**2) /
np.sum(eig_vals)**2)

return shape

Expand Down
11 changes: 8 additions & 3 deletions testsuite/MDAnalysisTests/core/test_atomgroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,8 @@ class TestUnwrapFlag(object):
np.array([[7333.79167791, -211.8997285, -721.50785456],
[-211.8997285, 7059.07470427, -91.32156884],
[-721.50785456, -91.32156884, 6509.31735029]]),
'asphericity': 0.02060121,
'asphericity': np.array([0.135, 0.047, 0.094]),
orbeckst marked this conversation as resolved.
Show resolved Hide resolved
'shape_parameter': np.array([-0.112, -0.004, 0.02]),
}

ref_Unwrap_residues = {
Expand All @@ -1009,7 +1010,8 @@ class TestUnwrapFlag(object):
'moment_of_inertia': np.array([[16687.941, -1330.617, 2925.883],
[-1330.617, 19256.178, 3354.832],
[2925.883, 3354.832, 8989.946]]),
'asphericity': 0.2969491080,
'asphericity': np.array([0.61 , 0.701, 0.381]),
orbeckst marked this conversation as resolved.
Show resolved Hide resolved
'shape_parameter': np.array([-0.461, 0.35 , 0.311]),
}

ref_noUnwrap = {
Expand All @@ -1019,6 +1021,7 @@ class TestUnwrapFlag(object):
[0.0, 98.6542, 0.0],
[0.0, 0.0, 98.65421327]]),
'asphericity': 1.0,
'shape_parameter': 1.0,
}

ref_Unwrap = {
Expand All @@ -1028,6 +1031,7 @@ class TestUnwrapFlag(object):
[0.0, 132.673, 0.0],
[0.0, 0.0, 132.673]]),
'asphericity': 1.0,
'shape_parameter': 1.0,
}

@pytest.fixture(params=[False, True]) # params indicate shuffling
Expand All @@ -1054,7 +1058,8 @@ def unwrap_group(self):
@pytest.mark.parametrize('method_name', ('center_of_geometry',
'center_of_mass',
'moment_of_inertia',
'asphericity'))
'asphericity',
'shape_parameter'))
def test_residues(self, ag, unwrap, ref, method_name):
method = getattr(ag, method_name)
if unwrap:
Expand Down