pytim/observables/basic_observables.py
# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding: utf-8 -*-
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
""" Module: basic_observables
=========================
"""
from __future__ import print_function
import numpy as np
from scipy import stats
from MDAnalysis.core.groups import Atom, AtomGroup, Residue, ResidueGroup
from MDAnalysis.lib import distances
from .observable import Observable
class Number(Observable):
"""The number of atoms.
"""
def __init__(self, *arg, **kwarg):
Observable.__init__(self, None)
def compute(self, inp, kargs=None):
"""Compute the observable.
:param AtomGroup inp: the input atom group
:returns: one, for each atom in the group
"""
return np.ones(len(inp))
class Mass(Observable):
"""Atomic masses"""
def __init__(self, *arg, **kwarg):
"""
>>> import pytim
>>> import MDAnalysis as mda
>>> from pytim.datafiles import WATERSMALL_GRO
>>> from pytim.observables import Mass
>>> u = mda.Universe(WATERSMALL_GRO)
>>> obs = Mass()
>>> print (obs.compute(u.atoms)[0])
15.999
"""
Observable.__init__(self, None)
def compute(self, inp, kargs=None):
"""Compute the observable.
:param AtomGroup inp: the input atom group
:returns: an array of masses for each atom in the group
"""
return inp.masses
class Charge(Observable):
"""Atomic charges"""
def __init__(self, *arg, **kwarg):
Observable.__init__(self, None)
def compute(self, inp, kargs=None):
"""Compute the observable.
:param AtomGroup inp: the input atom group
:returns: an array of charges for each atom in the group
"""
try:
return inp.charges
except AttributeError:
print("Error, the passed Atomgroup has no charges attribute")
class Vector(Observable):
def __init__(self, *arg, **kwarg):
Observable.__init__(self, None)
self.select_direction(arg[0])
class ReferenceFrame(Vector):
""" Given a set of triplets of atoms or positions, return the
corresponding set of reference frame units vectors, calculated
using the modified Gram-Schmidt (orientation preserving) or
using the faster QR decomposition (does not preserve orientation)
"""
def __init__(self, QR=False, *arg, **kwarg):
# TODO: does it need extension to 2- or 1- dimensional frames?
Vector.__init__(self, 'xyz', kwarg)
def compute(self, inp):
try:
pos = inp.atoms.positions
pos = pos.flatten()
except AttributeError:
try:
pos = inp.flatten()
except AttributeError:
raise ValueError('must pass an atom group or a ndarray')
n = pos.shape[0]
if np.mod(n, 9) > 0: # 3 atoms x 3 coordinates
raise ValueError('number of atoms in group not a multiple of 3')
n = n // 3
pos = pos.reshape(n, 3, 3)
if self.QR is True:
return np.asarray([np.linalg.qr(A)[0] for A in pos])
else:
return np.asarray([self._modified_GS(A) for A in pos])
def _modified_GS(self, A):
Q = np.zeros(A.shape)
for i in np.arange(A.shape[1]):
q = A[:, i]
for j in range(i):
q = q - np.dot(q, Q[:, j]) * Q[:, j]
Q[:, i] = q / la.norm(q)
return Q
class Distance(Observable):
""" Distance between atoms in two groups.
>>> import MDAnalysis as mda
>>> import pytim
>>> u = mda.Universe(pytim.datafiles.WATER_GRO)
>>> dist = pytim.observables.Distance()
>>> with np.printoptions(precision=3):
... print(dist.compute(u.atoms[:2],u.atoms[:2]) )
[0. 0.996 0.996 0. ]
The distance of the projections of the atoms position on
a plane can be computed by initializing the observable
in the same way as the Position one, e.g.:
>>> import MDAnalysis as mda
>>> import pytim
>>> u = mda.Universe(pytim.datafiles.WATER_GRO)
>>> dist = pytim.observables.Distance('xy')
>>> with np.printoptions(precision=3):
... print(dist.compute(u.atoms[:2],u.atoms[:2]) )
[0. 0.502 0.502 0. ]
Notice that the Distance observable is equivalent to using
RelativePosition passing a reference group and choosing to return
the radial component of the spherical coordinates. The latter
is, however, much slower than the former.
>>> import MDAnalysis as mda
>>> import pytim
>>> u = mda.Universe(pytim.datafiles.WATER_GRO)
>>> d1 = pytim.observables.Distance().compute(u.atoms[:9],u.atoms[:9])
>>> d2 = pytim.observables.RelativePosition(spherical=True).compute(u.atoms[:9],u.atoms[:9])[:,0]
>>> np.all(np.isclose(d1,d2))
True
>>> d1 = pytim.observables.Distance('xy').compute(u.atoms[:9],u.atoms[:9])
>>> d2 = pytim.observables.RelativePosition('xy',spherical=True).compute(u.atoms[:9],u.atoms[:9])[:,0]
>>> np.all(np.isclose(d1,d2))
True
"""
def __init__(self, arg='xyz', **kwarg):
Observable.__init__(self, None)
self.select_direction(arg)
def compute(self, inp, *args, **kwarg):
# TODO generalise to set of points?
"""Compute the observable.
:param AtomGroup inp: the input atom group
:returns: atomic positions
"""
inp = self._to_atomgroup(inp)
inp2 = self._to_atomgroup(args[0])
mask = np.asarray([0., 0., 0])
mask[self.dirmask] = 1.0
return distances.distance_array(
inp.positions * mask,
inp2.positions * mask,
box=inp.universe.dimensions).ravel()
class Position(Vector):
""" Atomic positions
Example: compute the projection on the xz plane of the first water molecule
>>> import MDAnalysis as mda
>>> import pytim
>>> u = mda.Universe(pytim.datafiles.WATER_GRO)
>>> proj = pytim.observables.Position('xz')
>>> with np.printoptions(precision=3):
... print(proj.compute(u.residues[0].atoms) )
[[28.62 11.37]
[28.42 10.51]
[29.61 11.51]]
"""
# TODO: add a flag to prevent recomputing the reference frame, in case
# it's constant
def __init__(self, arg='xyz', **kwarg):
Vector.__init__(self, arg, kwarg)
try:
self.folded = kwarg['folded']
except KeyError:
self.folded = True
try:
self.cartesian = kwarg['cartesian']
except KeyError:
self.cartesian = True
try:
self.spherical = kwarg['spherical']
except KeyError:
self.spherical = False
if self.spherical is True:
self.cartesian = False
def _cartesian_to_spherical(self, cartesian):
spherical = np.empty(cartesian.shape)
spherical[:, 0] = np.linalg.norm(cartesian, axis=1)
if len(self.dirmask) > 1: # 2d or 3d
spherical[:, 1] = np.arctan2(cartesian[:, 1], cartesian[:, 0])
if len(self.dirmask) == 3: # 3d
xy = np.sum(cartesian[:, [0, 1]]**2, axis=1)
spherical[:, 2] = np.arctan2(np.sqrt(xy), cartesian[:, 2])
return spherical
def compute(self, inp, **kwarg):
"""Compute the observable.
:param AtomGroup inp: the input atom group
:returns: atomic positions
"""
inp = self._to_atomgroup(inp)
box = inp.universe.dimensions[:3]
if self.folded:
cartesian = inp.atoms.pack_into_box()
# return cartesian[:,self.dirmask]
else:
cartesian = inp.positions
if self.cartesian:
return cartesian[:, self.dirmask]
if self.spherical:
return self._cartesian_to_spherical(cartesian)[:, self.dirmask]
class RelativePosition(Position):
def __init__(self, arg='xyz', **kwarg):
return Position.__init__(self, arg, **kwarg)
def compute(self, inp1, inp2, reference_frame=None):
# by adding a new axis we allow take the difference of positions
# of the N inp atoms to the M reference_group ones.
# pos here has dimension (N,M,3)
inp1 = self._to_atomgroup(inp1)
inp2 = self._to_atomgroup(inp2)
pos = inp1.positions[:, np.newaxis] - inp2.positions
if reference_frame is True:
ref = ReferenceFrame().compute(inp2)
pos = 0
elif reference_frame is not None:
try:
ref = ReferenceFrame().compute(reference_frame)
pos = 0
except:
raise ValueError(
'reference_frame can be None,True, or an atom group ')
# let's get back to dimension (NxM,dimension)
pos = pos[:, :, self.dirmask]
dimension = len(self.dirmask)
pos = pos.reshape((np.prod(pos.shape[:-1]), dimension))
box = inp1.universe.dimensions[self.dirmask]
if self.folded:
cond = np.where(pos > box / 2.)
pos[cond] -= box[cond[1]]
cond = np.where(pos < -box / 2.)
pos[cond] += box[cond[1]]
if self.cartesian:
return pos
if self.spherical:
return self._cartesian_to_spherical(pos)
class Velocity(Vector):
"""Atomic velocities"""
def __init__(self, arg='xyz', **kwarg):
Vector.__init__(self, arg)
def compute(self, inp, kargs=None):
"""Compute the observable.
:param AtomGroup inp: the input atom group
:returns: atomic velocities
"""
inp = self._to_atomgroup(inp)
return inp.velocities[:, self.dirmask]
class Force(Vector):
"""Atomic forces"""
def __init__(self, arg='xyz', **kwarg):
Vector.__init__(self, None)
def compute(self, inp, kargs=None):
"""Compute the observable.
:param AtomGroup inp: the input atom group
:returns: atomic forces
"""
inp = self._to_atomgroup(inp)
return inp.forces[:, self.dirmask]
class NumberOfResidues(Observable):
"""The number of residues.
Instead of associating 1 to the center of mass of the residue, we
associate 1/(number of atoms in residue) to each atom. In an
homogeneous system, these two definitions are (on average)
equivalent. If the system is not homogeneous, this is not true
anymore.
"""
def __init__(self, *arg, **karg):
Observable.__init__(self, None)
def compute(self, inp, kargs=None):
"""Compute the observable.
:param AtomGroup inp: the input atom group
:returns: one, for each residue in the group
"""
residue_names = np.unique(inp.atoms.resnames)
tmp = np.zeros(len(inp))
for name in residue_names:
atom_id = np.where(inp.resnames == name)[0][0]
tmp[inp.resnames == name] = 1. / len(inp[atom_id].residue.atoms)
return tmp