Source code for MDAnalysis.analysis.leaflet
# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*-
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
#
# MDAnalysis --- https://www.mdanalysis.org
# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors
# (see the file AUTHORS for the full list of names)
#
# Released under the Lesser GNU Public Licence, v2.1 or any higher version
#
# Please cite your use of MDAnalysis in published work:
#
# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler,
# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein.
# MDAnalysis: A Python package for the rapid analysis of molecular dynamics
# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th
# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy.
# doi: 10.25080/majora-629e541a-00e
#
# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein.
# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations.
# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787
#
"""
Leaflet identification --- :mod:`MDAnalysis.analysis.leaflet`
==============================================================
This module implements the *LeafletFinder* algorithm, described in
[Michaud-Agrawal2011]_. It can identify the lipids in a bilayer of
arbitrary shape and topology, including planar and undulating bilayers
under periodic boundary conditions or vesicles.
One can use this information to identify
* the upper and lower leaflet of a *planar membrane* by comparing the
the :meth:`~MDAnalysis.core.groups.AtomGroup.center_of_geometry` of
the leaflet groups, or
* the outer and inner leaflet of a *vesicle* by comparing histograms
of distances from the centre of geometry (or possibly simply the
:meth:`~MDAnalysis.core.groups.AtomGroup.radius_of_gyration`).
See example scripts in the MDAnalysisCookbook_ on how to use
:class:`LeafletFinder`. The function :func:`optimize_cutoff` implements a
(slow) heuristic method to find the best cut off for the LeafletFinder
algorithm.
.. _MDAnalysisCookbook: https://github.com/MDAnalysis/MDAnalysisCookbook/tree/master/examples
Algorithm
---------
1. build a graph of all phosphate distances < cutoff
2. identify the largest connected subgraphs
3. analyse first and second largest graph, which correspond to the leaflets
For further details see [Michaud-Agrawal2011]_.
Classes and Functions
---------------------
.. autoclass:: LeafletFinder
:members:
.. autofunction:: optimize_cutoff
"""
import warnings
import numpy as np
from .. import core
from . import distances
from .. import selections
from ..due import due, Doi
# networkx is an optional import
try:
import networkx as NX
except ImportError:
HAS_NX = False
else:
HAS_NX = True
due.cite(Doi("10.1002/jcc.21787"),
description="LeafletFinder algorithm",
path="MDAnalysis.analysis.leaflet",
cite_module=True)
del Doi
[docs]
class LeafletFinder(object):
"""Identify atoms in the same leaflet of a lipid bilayer.
This class implements the *LeafletFinder* algorithm [Michaud-Agrawal2011]_.
Parameters
----------
universe : Universe
:class:`~MDAnalysis.core.universe.Universe` object.
select : AtomGroup or str
A AtomGroup instance or a
:meth:`Universe.select_atoms` selection string
for atoms that define the lipid head groups, e.g.
universe.atoms.PO4 or "name PO4" or "name P*"
cutoff : float (optional)
head group-defining atoms within a distance of `cutoff`
Angstroms are deemed to be in the same leaflet [15.0]
pbc : bool (optional)
take periodic boundary conditions into account [``False``]
sparse : bool (optional)
``None``: use fastest possible routine; ``True``: use slow
sparse matrix implementation (for large systems); ``False``:
use fast :func:`~MDAnalysis.lib.distances.distance_array`
implementation [``None``].
Raises
------
ImportError
This class requires the optional package `networkx`. If not found
an ImportError is raised.
Example
-------
The components of the graph are stored in the list
:attr:`LeafletFinder.components`; the atoms in each component are numbered
consecutively, starting at 0. To obtain the atoms in the input structure
use :meth:`LeafletFinder.groups`::
u = mda.Universe(PDB)
L = LeafletFinder(u, 'name P*')
leaflet0 = L.groups(0)
leaflet1 = L.groups(1)
The residues can be accessed through the standard MDAnalysis mechanism::
leaflet0.residues
provides a :class:`~MDAnalysis.core.groups.ResidueGroup`
instance. Similarly, all atoms in the first leaflet are then ::
leaflet0.residues.atoms
.. versionchanged:: 1.0.0
Changed `selection` keyword to `select`
.. versionchanged:: 2.0.0
The universe keyword no longer accepts non-Universe arguments. Please
create a :class:`~MDAnalysis.core.universe.Universe` first.
"""
def __init__(self, universe, select, cutoff=15.0, pbc=False, sparse=None):
# Raise an error if networkx is not installed
if not HAS_NX:
errmsg = ("The LeafletFinder class requires an installation of "
"networkx. Please install networkx "
"https://networkx.org/documentation/stable/install.html")
raise ImportError(errmsg)
self.universe = universe
self.selectionstring = select
if isinstance(self.selectionstring, core.groups.AtomGroup):
self.selection = self.selectionstring
else:
self.selection = universe.select_atoms(self.selectionstring)
self.pbc = pbc
self.sparse = sparse
self._init_graph(cutoff)
def _init_graph(self, cutoff):
self.cutoff = cutoff
self.graph = self._get_graph()
self.components = self._get_components()
# The last two calls in _get_graph() and the single line in
# _get_components() are all that are needed to make the leaflet
# detection work.
def _get_graph(self):
"""Build graph from adjacency matrix at the given cutoff.
Automatically select between high and low memory usage versions of
contact_matrix."""
# could use self_distance_array to speed up but then need to deal with the sparse indexing
if self.pbc:
box = self.universe.trajectory.ts.dimensions
else:
box = None
coord = self.selection.positions
if self.sparse is False:
# only try distance array
try:
adj = distances.contact_matrix(coord, cutoff=self.cutoff, returntype="numpy", box=box)
except ValueError: # pragma: no cover
warnings.warn('N x N matrix too big, use sparse=True or sparse=None', category=UserWarning,
stacklevel=2)
raise
elif self.sparse is True:
# only try sparse
adj = distances.contact_matrix(coord, cutoff=self.cutoff, returntype="sparse", box=box)
else:
# use distance_array and fall back to sparse matrix
try:
# this works for small-ish systems and depends on system memory
adj = distances.contact_matrix(coord, cutoff=self.cutoff, returntype="numpy", box=box)
except ValueError: # pragma: no cover
# but use a sparse matrix method for larger systems for memory reasons
warnings.warn(
'N x N matrix too big - switching to sparse matrix method (works fine, but is currently rather '
'slow)',
category=UserWarning, stacklevel=2)
adj = distances.contact_matrix(coord, cutoff=self.cutoff, returntype="sparse", box=box)
return NX.Graph(adj)
def _get_components(self):
"""Return connected components (as sorted numpy arrays), sorted by size."""
return [np.sort(list(component)) for component in NX.connected_components(self.graph)]
[docs]
def update(self, cutoff=None):
"""Update components, possibly with a different *cutoff*"""
if cutoff is None:
cutoff = self.cutoff
self._init_graph(cutoff)
[docs]
def sizes(self):
"""Dict of component index with size of component."""
return dict(((idx, len(component)) for idx, component in enumerate(self.components)))
[docs]
def groups(self, component_index=None):
"""Return a :class:`MDAnalysis.core.groups.AtomGroup` for *component_index*.
If no argument is supplied, then a list of all leaflet groups is returned.
See Also
--------
:meth:`LeafletFinder.group`
:meth:`LeafletFinder.groups_iter`
"""
if component_index is None:
return list(self.groups_iter())
else:
return self.group(component_index)
[docs]
def group(self, component_index):
"""Return a :class:`MDAnalysis.core.groups.AtomGroup` for *component_index*."""
# maybe cache this?
indices = [i for i in self.components[component_index]]
return self.selection[indices]
[docs]
def groups_iter(self):
"""Iterator over all leaflet :meth:`groups`"""
for component_index in range(len(self.components)):
yield self.group(component_index)
[docs]
def write_selection(self, filename, **kwargs):
"""Write selections for the leaflets to *filename*.
The format is typically determined by the extension of *filename*
(e.g. "vmd", "pml", or "ndx" for VMD, PyMol, or Gromacs).
See :class:`MDAnalysis.selections.base.SelectionWriter` for all
options.
"""
sw = selections.get_writer(filename, kwargs.pop('format', None))
with sw(filename, mode=kwargs.pop('mode', 'w'),
preamble="leaflets based on select={selectionstring!r} cutoff={cutoff:f}\n".format(
**vars(self)),
**kwargs) as writer:
for i, ag in enumerate(self.groups_iter()):
name = "leaflet_{0:d}".format((i + 1))
writer.write(ag, name=name)
def __repr__(self):
return "<LeafletFinder({0!r}, cutoff={1:.1f} A) with {2:d} atoms in {3:d} groups>".format(
self.selectionstring, self.cutoff, self.selection.n_atoms,
len(self.components))
[docs]
def optimize_cutoff(universe, select, dmin=10.0, dmax=20.0, step=0.5,
max_imbalance=0.2, **kwargs):
r"""Find cutoff that minimizes number of disconnected groups.
Applies heuristics to find best groups:
1. at least two groups (assumes that there are at least 2 leaflets)
2. reject any solutions for which:
.. math::
\frac{|N_0 - N_1|}{|N_0 + N_1|} > \mathrm{max_imbalance}
with :math:`N_i` being the number of lipids in group
:math:`i`. This heuristic picks groups with balanced numbers of
lipids.
Parameters
----------
universe : Universe
:class:`MDAnalysis.Universe` instance
select : AtomGroup or str
AtomGroup or selection string as used for :class:`LeafletFinder`
dmin : float (optional)
dmax : float (optional)
step : float (optional)
scan cutoffs from `dmin` to `dmax` at stepsize `step` (in Angstroms)
max_imbalance : float (optional)
tuning parameter for the balancing heuristic [0.2]
kwargs : other keyword arguments
other arguments for :class:`LeafletFinder`
Returns
-------
(cutoff, N)
optimum cutoff and number of groups found
.. Note:: This function can die in various ways if really no
appropriate number of groups can be found; it ought to be
made more robust.
.. versionchanged:: 1.0.0
Changed `selection` keyword to `select`
"""
kwargs.pop('cutoff', None) # not used, so we filter it
_sizes = []
for cutoff in np.arange(dmin, dmax, step):
LF = LeafletFinder(universe, select, cutoff=cutoff, **kwargs)
# heuristic:
# 1) N > 1
# 2) no imbalance between large groups:
sizes = LF.sizes()
if len(sizes) < 2:
continue
n0 = float(sizes[0]) # sizes of two biggest groups ...
n1 = float(sizes[1]) # ... assumed to be the leaflets
imbalance = np.abs(n0 - n1) / (n0 + n1)
# print "sizes: %(sizes)r; imbalance=%(imbalance)f" % vars()
if imbalance > max_imbalance:
continue
_sizes.append((cutoff, len(LF.sizes())))
results = np.rec.fromrecords(_sizes, names="cutoff,N")
del _sizes
results.sort(order=["N", "cutoff"]) # sort ascending by N, then cutoff
return results[0] # (cutoff,N) with N>1 and shortest cutoff