Source code for prody.measure.transform

# -*- 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']


[docs]class Transformation(object): """A class for storing a transformation matrix.""" __slots__ = ['_matrix'] def __init__(self, *args): """Either 4x4 transformation *matrix*, or *rotation* matrix and *translation* vector must be provided at instantiation.""" nargs = len(args) if nargs == 1: self.setMatrix(args[0]) elif nargs == 2: self._matrix = np.eye(4) self.setRotation(args[0]) self.setTranslation(args[1]) else: raise ValueError('one or two array must be provided as arguments')
[docs] def getRotation(self): """Returns rotation matrix.""" if self._matrix is not None: return self._matrix[:3, :3]
[docs] def setRotation(self, rotation): """Set rotation matrix.""" if not isinstance(rotation, np.ndarray): raise TypeError('rotation must be an ndarray') elif rotation.shape != (3, 3): raise ValueError('rotation must be a 3x3 array') self._matrix[:3, :3] = rotation
[docs] def getTranslation(self): """Returns translation vector.""" if self._matrix is not None: return self._matrix[:3, 3]
[docs] def setTranslation(self, translation): """Set translation vector.""" if not isinstance(translation, np.ndarray): raise TypeError('translation must be an ndarray') elif translation.shape != (3,): raise ValueError('translation must be an ndarray of length 3') self._matrix[:3, 3] = translation
[docs] def getMatrix(self): """Returns a copy of the 4x4 transformation matrix whose top left is rotation matrix and last column is translation vector.""" if self._matrix is not None: return self._matrix.copy()
def setMatrix(self, matrix): if not isinstance(matrix, np.ndarray): raise TypeError('matrix must be an ndarray') elif matrix.shape != (4, 4): raise ValueError('matrix must have shape (4,4)') self._matrix = matrix
[docs] def apply(self, atoms): """Apply transformation to *atoms*, see :func:`applyTransformation` for details.""" return applyTransformation(self, atoms)
[docs]def calcTransformation(mobile, target, weights=None): """Returns a :class:`Transformation` instance which, when applied to the atoms in *mobile*, minimizes the weighted RMSD between *mobile* and *target*. *mobile* and *target* may be NumPy coordinate arrays, or :class:`.Atomic` instances, e.g. :class:`.AtomGroup`, :class:`.Chain`, or :class:`.Selection`.""" if not isinstance(mobile, np.ndarray): try: mob = mobile._getCoords() except AttributeError: raise TypeError('mobile must be a numpy array or an object ' 'with getCoords method') else: mob = mobile if not isinstance(target, np.ndarray): try: tar = target._getCoords() except AttributeError: raise TypeError('target must be a numpy array or an object ' 'with getCoords method') else: tar = target if mob.shape != tar.shape: raise ValueError('reference and target coordinate arrays ' 'must have same number of atoms') if mob.shape[1] != 3: raise ValueError('reference and target must be coordinate arrays') if weights is None: if isinstance(mobile, AtomMap): LOGGER.warn('mobile is an AtomMap instance, consider assign weights=mobile.getFlags("mapped") ' 'if there are dummy atoms in mobile') if isinstance(target, AtomMap): LOGGER.warn('target is an AtomMap instance, consider assign weights=target.getFlags("mapped") ' 'if there are dummy atoms in target') if weights is not None: weights = checkWeights(weights, mob.shape[0]) return Transformation(*getTransformation(mob, tar, weights))
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)
[docs]def applyTransformation(transformation, atoms): """Returns *atoms* after applying *transformation*. If *atoms* is a :class:`.Atomic` instance, it will be returned after *transformation* is applied to its active coordinate set. If *atoms* is an :class:`.AtomPointer` instance, *transformation* will be applied to the corresponding coordinate set in the associated :class:`.AtomGroup`.""" coords = None ag = None if isinstance(atoms, np.ndarray): atoms = np.atleast_2d(atoms) if atoms.shape[1] != 3: raise ValueError('atoms must be a 3-d coordinate array') coords = atoms atoms = None else: if isinstance(atoms, AtomPointer): ag = atoms.getAtomGroup() acsi = ag.getACSIndex() ag.setACSIndex(atoms.getACSIndex()) coords = ag._getCoords() else: try: coords = atoms._getCoords() except AttributeError: raise TypeError('atoms must be a Atomic instance') if atoms is None: return _applyTransformation(transformation, coords) else: if ag is None: atoms.setCoords(_applyTransformation(transformation, coords)) else: ag.setCoords(_applyTransformation(transformation, coords)) ag.setACSIndex(acsi) return atoms
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