# -*- coding: utf-8 -*-
""" This module defines a class for identifying contacts."""
import numpy as np
from prody import LOGGER
from prody.atomic import AtomPointer, AtomMap
from prody.utilities import importLA, checkWeights
from .measure import calcCenter
linalg = importLA()
__all__ = ['Transformation', 'applyTransformation', 'alignCoordsets',
'calcRMSD', 'calcTransformation', 'superpose',
'moveAtoms', 'wrapAtoms',
'printRMSD']
def getTransformation(mob, tar, weights=None):
if weights is None:
mob_com = mob.mean(0)
tar_com = tar.mean(0)
mob = mob - mob_com
tar = tar - tar_com
matrix = np.dot(mob.T, tar)
else:
weights_sum = weights.sum()
weights_dot = np.dot(weights.T, weights)
mob_com = (mob * weights).sum(axis=0) / weights_sum
tar_com = (tar * weights).sum(axis=0) / weights_sum
mob = mob - mob_com
tar = tar - tar_com
matrix = np.dot((mob * weights).T, (tar * weights)) / weights_dot
U, _, Vh = linalg.svd(matrix)
d = np.sign(linalg.det(np.dot(U, Vh)))
Id = np.array([[1, 0, 0],
[0, 1, 0],
[0, 0, d]])
rotation = np.dot(Vh.T, np.dot(Id, U.T))
return rotation, tar_com - np.dot(mob_com, rotation.T)
def _applyTransformation(t, coords):
return np.dot(coords, t.getRotation().T) + t.getTranslation()
[docs]def superpose(mobile, target, weights=None):
"""Returns *mobile*, after its RMSD minimizing superposition onto *target*,
and the transformation that minimizes the RMSD."""
t = calcTransformation(mobile, target, weights)
result = applyTransformation(t, mobile)
return (result, t)
[docs]def moveAtoms(atoms, **kwargs):
"""Move *atoms* *to* a new location or *by* an offset. This method will
change the active coordinate set of the *atoms*. Note that only one of
*to* or *by* keyword arguments is expected.
Move protein so that its centroid is at the origin, ``[0., 0., 0.]``:
.. ipython:: python
from prody import *
from numpy import ones, zeros
protein = parsePDB('1ubi')
calcCenter(protein).round(3)
moveAtoms(protein, to=zeros(3))
calcCenter(protein).round(3)
Move protein so that its mass center is at the origin:
.. ipython:: python
protein.setMasses(ones(len(protein)))
protein.carbon.setMasses(12)
protein.nitrogen.setMasses(14)
protein.oxygen.setMasses(16)
moveAtoms(protein, to=zeros(3), weights=protein.getMasses())
calcCenter(protein, weights=protein.getMasses()).round(3)
Move protein so that centroid of Cα atoms is at the origin:
.. ipython:: python
moveAtoms(protein.ca, to=zeros(3), ag=True)
calcCenter(protein).round(3)
calcCenter(protein.ca).round(3)
Move protein by 10 A along each direction:
.. ipython:: python
moveAtoms(protein, by=ones(3) * 10)
calcCenter(protein).round(3)
calcCenter(protein.ca).round(3)
:arg by: an offset array with shape ``([1,] 3)`` or ``(n_atoms, 3)`` or
a transformation matrix with shape ``(4, 4)``
:type by: :class:`numpy.ndarray`
:arg to: a point array with shape ``([1,] 3)``
:type to: :class:`numpy.ndarray`
:arg ag: when *atoms* is a :class:`.AtomSubset`, apply translation vector
(*to*) or transformation matrix to the :class:`.AtomGroup`,
default is **False**
:type ag: bool
:arg weights: array of atomic weights with shape ``(n_atoms[, 1])``
:type weights: :class:`numpy.ndarray`
When *to* argument is passed, :func:`.calcCenter` function is used to
calculate centroid or mass center."""
point = kwargs.pop('to', None)
if point is None:
offset = kwargs.pop('by', None)
if offset is None:
raise TypeError('moveAtoms() expects one of {0} or {1} '
'arguments'.format(repr('to'), repr('by')))
try:
shape = offset.shape
except AttributeError:
raise TypeError('by must be a numpy array')
if shape == (4, 4):
if kwargs.pop('ag', False):
try:
atoms = atoms.getAtomGroup()
except AttributeError:
# itself must be an AtomGroup
pass
try:
coords = atoms._getCoords()
except AttributeError:
try:
coords = atoms.getCoords()
except AttributeError:
raise TypeError('atoms must be an Atomic instance')
coords = np.dot(coords, offset[:3, :3])
coords += offset[3, :3]
atoms.setCoords(coords)
else:
try:
coords = atoms._getCoords()
except AttributeError:
try:
coords = atoms.getCoords()
except AttributeError:
raise TypeError('atoms must be an Atomic instance')
try:
natoms = atoms.numAtoms()
except AttributeError:
raise TypeError('atoms must be an Atomic instance')
if shape == (3,) or shape == (1, 3) or shape == (natoms, 3):
atoms.setCoords(coords + offset)
else:
raise ValueError('by.shape is not valid')
else:
try:
shape = point.shape
except AttributeError:
raise TypeError('to must be a numpy array')
if shape != (3,) and shape != (1, 3):
raise ValueError('to.shape must be ([1,] 3)')
center = calcCenter(atoms, weights=kwargs.pop('weights', None))
offset = point - center
if kwargs.pop('ag', False):
try:
atoms = atoms.getAtomGroup()
except AttributeError:
# itself must be an AtomGroup
pass
try:
coords = atoms._getCoords()
except AttributeError:
try:
coords = atoms.getCoords()
except AttributeError:
raise TypeError('atoms must be an Atomic instance')
atoms.setCoords(coords + offset)
[docs]def calcRMSD(reference, target=None, weights=None):
"""Returns root-mean-square deviation (RMSD) between reference and target
coordinates.
.. ipython:: python
ens = loadEnsemble('p38_X-ray.ens.npz')
ens.getRMSDs()
calcRMSD(ens)
calcRMSD(ens.getCoords(), ens.getCoordsets(), ens.getWeights())"""
if isinstance(reference, np.ndarray):
ref = reference
else:
try:
ref = reference._getCoords()
except AttributeError:
raise TypeError('reference must be a numpy array or an object '
'with getCoords method')
if target is None:
try:
target = reference._getCoordsets()
except AttributeError:
pass
if weights is None:
try:
weights = reference._getWeights()
except AttributeError:
pass
if ref.ndim != 2 or ref.shape[1] != 3:
raise ValueError('reference must have shape (n_atoms, 3)')
if isinstance(target, np.ndarray):
tar = target
else:
try:
tar = target._getCoords()
except AttributeError:
raise TypeError('target must be a numpy array or an object '
'with getCoords method')
if tar.ndim not in (2, 3) or tar.shape[-1] != 3:
raise ValueError('target must have shape ([n_confs,] n_atoms, 3)')
if ref.shape != tar.shape[-2:]:
raise ValueError('reference and target arrays must have the same '
'number of atoms')
if weights is not None:
n_atoms = len(ref)
n_csets = 1 if tar.ndim == 2 else len(tar)
weights = checkWeights(weights, n_atoms, n_csets)
return getRMSD(ref, tar, weights)
def getRMSD(ref, tar, weights=None):
if weights is None:
divByN = 1.0 / ref.shape[0]
if tar.ndim == 2:
return np.sqrt(((ref-tar) ** 2).sum() * divByN)
else:
rmsd = np.zeros(len(tar))
for i, t in enumerate(tar):
rmsd[i] = ((ref-t) ** 2).sum()
return np.sqrt(rmsd * divByN)
else:
if tar.ndim == 2:
return np.sqrt((((ref-tar) ** 2) * weights).sum() *
(1. / weights.sum()))
else:
rmsd = np.zeros(len(tar))
if weights.ndim == 2:
for i, t in enumerate(tar):
rmsd[i] = (((ref-t) ** 2) * weights).sum()
return np.sqrt(rmsd * (1. / weights.sum()))
else:
for i, t in enumerate(tar):
rmsd[i] = (((ref-t) ** 2) * weights[i]).sum()
return np.sqrt(rmsd / weights.sum(1).flatten())
[docs]def printRMSD(reference, target=None, weights=None, log=True, msg=None):
"""Print RMSD to the screen. If *target* has multiple coordinate sets,
minimum, maximum and mean RMSD values are printed. If *log* is **True**
(default), RMSD is written to the standard error using package logger,
otherwise standard output is used. When *msg* string is given, it is
printed before the RMSD value. See also :func:`calcRMSD` function. """
if log:
write = LOGGER.info
else:
import sys
write = lambda line: sys.stdout.write(line + '\n')
msg = msg or ''
if msg and msg[-1] != ' ':
msg += ' '
rmsd = calcRMSD(reference, target, weights)
if isinstance(rmsd, np.ndarray) and len(rmsd) > 1:
write(msg + 'RMSD: min={0:.2f}, max={1:.2f}, mean={2:.2f}'
.format(rmsd.min(), rmsd.max(), rmsd.mean()))
else:
if isinstance(rmsd, np.ndarray):
rmsd = rmsd[0]
write(msg + 'RMSD: {0:.2f}'.format(rmsd))
[docs]def alignCoordsets(atoms, weights=None):
"""Returns *atoms* after superposing coordinate sets onto its active
coordinate set. Transformations will be calculated for *atoms* and
applied to its :class:`.AtomGroup`, when applicable. Optionally,
atomic *weights* can be passed for weighted superposition."""
try:
acsi, n_csets = atoms.getACSIndex(), atoms.numCoordsets()
except AttributeError:
raise TypeError('atoms must have type Atomic, not {0}'
.format(type(atoms)))
if n_csets < 2:
LOGGER.warning('{0} contains fewer than two coordinate sets, '
'alignment was not performed.'.format(str(atoms)))
return
try:
ag = atoms.getAtomGroup()
except AttributeError:
ag = atoms
agacsi = ag.getACSIndex()
tar = atoms._getCoords()
for i in range(n_csets):
if i == acsi:
continue
atoms.setACSIndex(i)
ag.setACSIndex(i)
calcTransformation(atoms, tar, weights).apply(ag)
atoms.setACSIndex(acsi)
ag.setACSIndex(agacsi)
return atoms
[docs]def wrapAtoms(frame, unitcell=None, center=np.array([0., 0., 0.])):
"""Wrap atoms into an image of the system simulated under periodic boundary
conditions. When *frame* is a :class:`.Frame`, unitcell information will be
retrieved automatically.
.. note::
This function will wrap all atoms into the specified periodic image, so
covalent bonds will be broken.
:arg frame: a frame instance or a coordinate set
:type frame: :class:`.Frame`, :class:`.AtomGroup`, :class:`numpy.ndarray`
:arg unitcell: orthorhombic unitcell array with shape (3,)
:type unitcell: :class:`numpy.ndarray`
:arg center: coordinates of the center of the wrapping cell, default is
the origin of the Cartesian coordinate system
:type center: :class:`numpy.ndarray`"""
try:
coords = frame._getCoords()
except AttributeError:
coords = frame
else:
try:
frame.getAtomGroup()
except AttributeError:
pass
else:
raise TypeError('frame must be a Frame, AtomGroup, or numpy array,'
' not a ' + str(type(frame)))
if unitcell is None:
try:
unitcell = frame.getUnitcell()[:3]
except AttributeError:
raise TypeError('unitcell information must be provided')
half = unitcell / 2
ucmin = center - half
ucmax = center + half
for axis in range(3):
xyz = coords[:, axis]
which = (xyz < ucmin[axis]).nonzero()[0]
while len(which):
coords[which, axis] += unitcell[axis]
which = which[coords[which, axis] < ucmin[axis]]
which = (xyz > ucmax[axis]).nonzero()[0]
while len(which):
coords[which, axis] -= unitcell[axis]
which = which[coords[which, axis] > ucmax[axis]]
return frame