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

New Dihedral and Ramachandran Analysis #1997

Merged
merged 20 commits into from
Jul 25, 2018
Merged
Show file tree
Hide file tree
Changes from 14 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
105 changes: 105 additions & 0 deletions package/MDAnalysis/analysis/dihedrals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import numpy as np
Copy link
Member

Choose a reason for hiding this comment

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

Please add the standard header – copy from the top of rms.py

Copy link
Member

Choose a reason for hiding this comment

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

Please add from __future__ import absolute_import as the very first import with any import from six.moves directly below it. Also include the range import from six.moves

import matplotlib.pyplot as plt
import warnings
import six
from six.moves import zip

import MDAnalysis as mda
from MDAnalysis.analysis.base import AnalysisBase
from MDAnalysis.lib.distances import calc_dihedrals


class Ramachandran(AnalysisBase):
"""Calculate phi and psi dihedral angles of specified residues.

Note
----
Run the analysis with :meth:`Ramachandran.run()`, which stores the results
in the array :attr:`Ramachandran.angles`. A axes object can be obtained
with :meth: `Ramachandran.run().plot()`.

If the residue selection is beyond the scope of the protein, then an error
will be raised. If the residue selection includes the first or last residue
then a warning will be raised, and the final array of angles will not
include those residues.

"""
def __init__(self, atomgroup, **kwargs):
r"""Parameters
----------
atomgroup : Atomgroup
atoms for residues for which phi and psi are calculated
start : int, optional
starting frame, default None becomes 0.
stop : int, optional
Copy link
Member

Choose a reason for hiding this comment

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

aren't we moving those to the run function @orbeckst

Copy link
Member

Choose a reason for hiding this comment

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

yes, you're right, of course – I asked @hfmull to start from analysis.rms.RMSD and forgot that it uses soon-to-be changed call signatures (#1463).

Do we need to keep init start/stop/step for new additions like this one and deprecate them (#1979) right away?? I would say no (don't teach the user something you want them to forget) but I am open to alternative opinions.

Copy link
Member

Choose a reason for hiding this comment

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

still keeping them here makes this behave exactly as any other class. Kind of nice.

Copy link
Member

Choose a reason for hiding this comment

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

It makes more sense to not allow them here if we want them in run

Frame index to stop analysis. Default: None becomes
n_frames. Iteration stops *before* this frame number,
which means that the trajectory would be read until the end.
step : int, optional
step between frames, default None becomes 1.

"""
super(Ramachandran, self).__init__(atomgroup.universe.trajectory, **kwargs)
self.atomgroup = atomgroup
residues = self.atomgroup.residues
protein = self.atomgroup.universe.select_atoms("protein").residues

if not residues.issubset(protein):
raise IndexError("Found atoms outside of protein. Only atoms "
Copy link
Member

Choose a reason for hiding this comment

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

I would use a value error here. The IndexError is when you use the square bracket operator.

"inside of a 'protein' selection can be used to "
"calculate dihedrals.")
elif not residues.isdisjoint(protein[[0, -1]]):
warnings.warn("Cannot determine phi and psi angles for the first "
"or last residues")
residues = residues.difference(protein[[0, -1]])

phi_sel = [res.phi_selection() for res in residues]
psi_sel = [res.psi_selection() for res in residues]
self.ag1 = mda.AtomGroup([atoms[0] for atoms in phi_sel])
self.ag2 = mda.AtomGroup([atoms[1] for atoms in phi_sel])
self.ag3 = mda.AtomGroup([atoms[2] for atoms in phi_sel])
self.ag4 = mda.AtomGroup([atoms[3] for atoms in phi_sel])
self.ag5 = mda.AtomGroup([atoms[3] for atoms in psi_sel])

def _prepare(self):
self.angles = []

def _single_frame(self):
phi_angles = calc_dihedrals(self.ag1.positions, self.ag2.positions,
self.ag3.positions, self.ag4.positions,
box=self.ag1.dimensions)
psi_angles = calc_dihedrals(self.ag2.positions, self.ag3.positions,
self.ag4.positions, self.ag5.positions,
box=self.ag1.dimensions)
phi_psi = [(phi, psi) for phi, psi in zip(phi_angles, psi_angles)]
self.angles.append(phi_psi)

def _conclude(self):
self.angles = np.rad2deg(np.array(self.angles))


def plot(self, ax=None, **kwargs):
"""Plots data into standard ramachandran plot. Each time step in
self.angles is plotted onto the same graph.

Parameters
----------
ax : :class:`matplotlib.axes.Axes`
If no `ax` is supplied or set to ``None`` then the plot will
be added to the current active axes.

Returns
-------
ax : :class:`~matplotlib.axes.Axes`
Axes with the plot, either `ax` or the current axes.

"""
if ax is None:
ax = plt.gca()
ax.axis([-180,180,-180,180])
Copy link
Member

Choose a reason for hiding this comment

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

How do people plot the allowed and marginally allowed regions in "standard" Ramachandran plots? Is there a simple function (something like a spline for a density that gives the correct contours) or a dataset that we could use?

It would be nice if we had a helper function to plot them; this method could then use the helper to plot these regions in the background but anyone else could use it, too.

(Not a requirement for this PR but something to think about.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found a data set from this website that was taken from this paper that I was able to make a decent reference graph of. I just don't know where I should put the data need to graph it and where I should cite it. This is just an example of what it looks like with the reference.
rama_ref_demo

ax.axhline(0, color='k', lw=1)
ax.axvline(0, color='k', lw=1)
ax.set(xticks=range(-180,181,60), yticks=range(-180,181,60),
xlabel=r"$\phi$ (deg)", ylabel=r"$\psi$ (deg)")
a = self.angles.reshape(np.prod(self.angles.shape[:2]), 2)
ax.scatter(a[:,0], a[:,1])
Copy link
Member

Choose a reason for hiding this comment

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

You need to add **kwargs to the scatter function as an argument.

39 changes: 39 additions & 0 deletions testsuite/MDAnalysisTests/analysis/test_dihedrals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import numpy as np
Copy link
Member

Choose a reason for hiding this comment

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

The copyright header should also be added here

Copy link
Member

Choose a reason for hiding this comment

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

pylint will also issue a warning here to add from __future__ import absolute_import as the top most import

from numpy.testing import assert_almost_equal
import pytest

import MDAnalysis as mda
from MDAnalysisTests.datafiles import GRO, XTC, DihedralsArray, GLYDihedralsArray
from MDAnalysis.analysis import dihedrals


class TestRamachandran(object):

@pytest.fixture()
def universe(self):
return mda.Universe(GRO, XTC)

def test_ramachandran(self, universe):
rama = dihedrals.Ramachandran(universe.select_atoms("protein")).run()
test_rama = np.load(DihedralsArray)

assert_almost_equal(rama.angles, test_rama, 5,
err_msg="error: dihedral angles should "
"match test values")

def test_ramachandran_single_frame(self, universe):
rama = dihedrals.Ramachandran(universe.select_atoms("protein"),
start=5, stop=6).run()
test_rama = [np.load(DihedralsArray)[5]]

assert_almost_equal(rama.angles, test_rama, 5,
err_msg="error: dihedral angles should "
"match test values")

def test_ramachandran_residue_selections(self, universe):
rama = dihedrals.Ramachandran(universe.select_atoms("resname GLY")).run()
test_rama = np.load(GLYDihedralsArray)

assert_almost_equal(rama.angles, test_rama, 5,
err_msg="error: dihedral angles should "
"match test values")
Binary file not shown.
Binary file not shown.
6 changes: 5 additions & 1 deletion testsuite/MDAnalysisTests/datafiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@
"legacy_DCD_NAMD_coords", # frame 0 read in for SiN_tric_namd.dcd using legacy DCD reader
"legacy_DCD_c36_coords", # frames 1 and 4 read in for tip125_tric_C36.dcd using legacy DCD reader
"GSD",
"GRO_MEMPROT", "XTC_MEMPROT" # YiiP transporter in POPE:POPG lipids with Na+, Cl-, Zn2+ dummy model without water
"GRO_MEMPROT", "XTC_MEMPROT", # YiiP transporter in POPE:POPG lipids with Na+, Cl-, Zn2+ dummy model without water
"DihedralsArray", "GLYDihedralsArray" # phi and psi angles for testing Ramachandran class
]

from pkg_resources import resource_filename
Expand Down Expand Up @@ -415,5 +416,8 @@

GSD = resource_filename(__name__, 'data/example.gsd')

DihedralsArray = resource_filename(__name__, 'data/adk_oplsaa_dihedrals.npy')
Copy link
Member

Choose a reason for hiding this comment

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

You also need to add the to __all__ (I only learned that from the test that failed!)

GLYDihedralsArray = resource_filename(__name__, 'data/adk_oplsaa_GLY_dihedrals.npy')

# This should be the last line: clean up namespace
del resource_filename