def test_CalcRMSDRotationalMatrix(): # Setup coordinates frag_a = np.zeros((3, 7), dtype=np.float64) frag_b = np.zeros((3, 7), dtype=np.float64) N = 7 frag_a[0][0] = -2.803 frag_a[1][0] = -15.373 frag_a[2][0] = 24.556 frag_a[0][1] = 0.893 frag_a[1][1] = -16.062 frag_a[2][1] = 25.147 frag_a[0][2] = 1.368 frag_a[1][2] = -12.371 frag_a[2][2] = 25.885 frag_a[0][3] = -1.651 frag_a[1][3] = -12.153 frag_a[2][3] = 28.177 frag_a[0][4] = -0.440 frag_a[1][4] = -15.218 frag_a[2][4] = 30.068 frag_a[0][5] = 2.551 frag_a[1][5] = -13.273 frag_a[2][5] = 31.372 frag_a[0][6] = 0.105 frag_a[1][6] = -11.330 frag_a[2][6] = 33.567 frag_b[0][0] = -14.739 frag_b[1][0] = -18.673 frag_b[2][0] = 15.040 frag_b[0][1] = -12.473 frag_b[1][1] = -15.810 frag_b[2][1] = 16.074 frag_b[0][2] = -14.802 frag_b[1][2] = -13.307 frag_b[2][2] = 14.408 frag_b[0][3] = -17.782 frag_b[1][3] = -14.852 frag_b[2][3] = 16.171 frag_b[0][4] = -16.124 frag_b[1][4] = -14.617 frag_b[2][4] = 19.584 frag_b[0][5] = -15.029 frag_b[1][5] = -11.037 frag_b[2][5] = 18.902 frag_b[0][6] = -18.577 frag_b[1][6] = -10.001 frag_b[2][6] = 17.996 # Allocate rotation array rot = np.zeros((9, ), dtype=np.float64) # Calculate center of geometry comA = np.sum(frag_a, axis=1) / N comB = np.sum(frag_b, axis=1) / N # Center each fragment frag_a = frag_a - comA.reshape(3, 1) frag_b = frag_b - comB.reshape(3, 1) # Calculate rmsd and rotation matrix qcp_rmsd = qcp.CalcRMSDRotationalMatrix(frag_a.T, frag_b.T, N, rot, None) #print 'qcp rmsd = ',rmsd #print 'rotation matrix:' #print rot.reshape((3,3)) # rotate frag_b to obtain optimal alignment frag_br = frag_b.T * np.matrix(rot.reshape((3, 3))) aligned_rmsd = rmsd(frag_br.T, frag_a) #print 'rmsd after applying rotation: ',rmsd assert_almost_equal( aligned_rmsd, 0.719106, 6, "RMSD between fragments A and B does not match excpected value.") expected_rot = np.array([[0.72216358, -0.52038257, -0.45572112], [0.69118937, 0.51700833, 0.50493528], [-0.0271479, -0.67963547, 0.73304748]]) assert_almost_equal( rot.reshape((3, 3)), expected_rot, 6, "Rotation matrix for aliging B to A does not have expected values.")
def rmsd(a, b, weights=None, center=False, superposition=False): """Returns RMSD between two coordinate sets `a` and `b`. `a` and `b` are arrays of the coordinates of N atoms of shape N*3 as generated by, e.g., :meth:`MDAnalysis.core.AtomGroup.AtomGroup.coordinates`. Parameters ---------- a, b : array_like coordinates to align weights : array_like (optional) 1D array with weights, use to compute weighted average center : bool (optional) subtract center of geometry before calculation. With weights given compute weighted average as center. superposition : bool (optional) perform a rotational and translational superposition with the fast QCP algorithm [Theobald2005]_ before calculating the RMSD Returns ------- rmsd : float RMSD between a and b Example ------- >>> u = Universe(PSF,DCD) >>> bb = u.select_atoms('backbone') >>> A = bb.positions.copy() # coordinates of first frame >>> u.trajectory[-1] # forward to last frame >>> B = bb.positions.copy() # coordinates of last frame >>> rmsd(A, B, center=True) 3.9482355416565049 .. versionchanged: 0.8.1 *center* keyword added .. versionchanged: 0.14.0 *superposition* keyword added """ a = np.asarray(a, dtype=np.float64) b = np.asarray(b, dtype=np.float64) N = b.shape[0] if a.shape != b.shape: raise ValueError('a and b must have same shape') if weights is not None: if len(weights) != len(a): raise ValueError('weights must have same length as a/b') # weights are constructed as relative to the mean relative_weights = np.asarray(weights) / np.mean(weights) else: relative_weights = None # superposition only works if structures are centered if center or superposition: # make copies (do not change the user data!) # weights=None is equivalent to all weights 1 a = a - np.average(a, axis=0, weights=weights) b = b - np.average(b, axis=0, weights=weights) if superposition: return qcp.CalcRMSDRotationalMatrix(a.T, b.T, N, None, relative_weights) else: return np.sqrt(np.sum((a - b)**2) / a.size)
def run(self, **kwargs): """Perform RMSD analysis on the trajectory. A number of parameters can be changed from the defaults. The result is stored as the array :attr:`RMSD.rmsd`. :Keywords: *start*, *stop*, *step* start and stop frame index with step size: analyse ``trajectory[start:stop:step]`` [``None``] *mass_weighted* do a mass-weighted RMSD fit *tol_mass* Reject match if the atomic masses for matched atoms differ by more than *tol_mass* *ref_frame* frame index to select frame from *reference* """ from itertools import izip start = kwargs.pop('start', None) stop = kwargs.pop('stop', None) step = kwargs.pop('step', None) mass_weighted = kwargs.pop('mass_weighted', self.mass_weighted) ref_frame = kwargs.pop('ref_frame', self.ref_frame) natoms = self.traj_atoms.n_atoms trajectory = self.universe.trajectory traj_atoms = self.traj_atoms if mass_weighted: # if performing a mass-weighted alignment/rmsd calculation weight = self.ref_atoms.masses / self.ref_atoms.masses.mean() else: weight = None # reference centre of mass system current_frame = self.reference.trajectory.ts.frame - 1 try: # Move to the ref_frame # (coordinates MUST be stored in case the ref traj is advanced elsewhere or if ref == mobile universe) self.reference.trajectory[ref_frame] ref_com = self.ref_atoms.center_of_mass() ref_coordinates = self.ref_atoms.positions - ref_com # makes a copy if self.groupselections_atoms: groupselections_ref_coords_T_64 = [ self.reference.select_atoms( *s['reference']).positions.T.astype(np.float64) for s in self.groupselections ] finally: # Move back to the original frame self.reference.trajectory[current_frame] ref_coordinates_T_64 = ref_coordinates.T.astype(np.float64) # allocate the array for selection atom coords traj_coordinates = traj_atoms.coordinates().copy() if self.groupselections_atoms: # Only carry out a rotation if we want to calculate secondary RMSDs. # R: rotation matrix that aligns r-r_com, x~-x~com # (x~: selected coordinates, x: all coordinates) # Final transformed traj coordinates: x' = (x-x~_com)*R + ref_com rot = np.zeros(9, dtype=np.float64) # allocate space for calculation R = np.matrix(rot.reshape(3, 3)) else: rot = None # RMSD timeseries nframes = len(np.arange(0, len(trajectory))[start:stop:step]) rmsd = np.zeros((nframes, 3 + len(self.groupselections_atoms))) percentage = ProgressMeter( nframes, interval=10, format= "RMSD %(rmsd)5.2f A at frame %(step)5d/%(numsteps)d [%(percentage)5.1f%%]\r" ) for k, ts in enumerate(trajectory[start:stop:step]): # shift coordinates for rotation fitting # selection is updated with the time frame x_com = traj_atoms.center_of_mass().astype(np.float32) traj_coordinates[:] = traj_atoms.coordinates() - x_com rmsd[k, :2] = ts.frame, trajectory.time if self.groupselections_atoms: # 1) superposition structures # Need to transpose coordinates such that the coordinate array is # 3xN instead of Nx3. Also qcp requires that the dtype be float64 # (I think we swapped the position of ref and traj in CalcRMSDRotationalMatrix # so that R acts **to the left** and can be broadcasted; we're saving # one transpose. [orbeckst]) rmsd[k, 2] = qcp.CalcRMSDRotationalMatrix( ref_coordinates_T_64, traj_coordinates.T.astype(np.float64), natoms, rot, weight) R[:, :] = rot.reshape(3, 3) # Transform each atom in the trajectory (use inplace ops to avoid copying arrays) # (Marginally (~3%) faster than "ts.positions[:] = (ts.positions - x_com) * R + ref_com".) ts.positions -= x_com ts.positions[:] = ts.positions * R # R acts to the left & is broadcasted N times. ts.positions += ref_com # 2) calculate secondary RMSDs for igroup, (refpos, atoms) in enumerate( izip(groupselections_ref_coords_T_64, self.groupselections_atoms), 3): rmsd[k, igroup] = qcp.CalcRMSDRotationalMatrix( refpos, atoms['mobile'].positions.T.astype(np.float64), atoms['mobile'].n_atoms, None, weight) else: # only calculate RMSD by setting the Rmatrix to None # (no need to carry out the rotation as we already get the optimum RMSD) rmsd[k, 2] = qcp.CalcRMSDRotationalMatrix( ref_coordinates_T_64, traj_coordinates.T.astype(np.float64), natoms, None, weight) percentage.echo(ts.frame, rmsd=rmsd[k, 2]) self.rmsd = rmsd
def rms_fit_trj(traj, reference, select='all', filename=None, rmsdfile=None, prefix='rmsfit_', mass_weighted=False, tol_mass=0.1, strict=False, force=True, quiet=False, **kwargs): """RMS-fit trajectory to a reference structure using a selection. Both reference *ref* and trajectory *traj* must be :class:`MDAnalysis.Universe` instances. If they contain a trajectory then it is used. The output file format is determined by the file extension of *filename*. One can also use the same universe if one wants to fit to the current frame. :Arguments: *traj* trajectory, :class:`MDAnalysis.Universe` object *reference* reference coordinates; :class:`MDAnalysis.Universe` object (uses the current time step of the object) *select* 1. any valid selection string for :meth:`~MDAnalysis.core.AtomGroup.AtomGroup.select_atoms` that produces identical selections in *mobile* and *reference*; or 2. a dictionary ``{'mobile':sel1, 'reference':sel2}`` (the :func:`fasta2select` function returns such a dictionary based on a ClustalW_ or STAMP_ sequence alignment); or 3. a tuple ``(sel1, sel2)`` When using 2. or 3. with *sel1* and *sel2* then these selections can also each be a list of selection strings (to generate a AtomGroup with defined atom order as described under :ref:`ordered-selections-label`). *filename* file name for the RMS-fitted trajectory or pdb; defaults to the original trajectory filename (from *traj*) with *prefix* prepended *rmsdfile* file name for writing the RMSD timeseries [``None``] *prefix* prefix for autogenerating the new output filename *mass_weighted* do a mass-weighted RMSD fit *tol_mass* Reject match if the atomic masses for matched atoms differ by more than *tol_mass* [0.1] *strict* Default: ``False`` - ``True``: Will raise :exc:`SelectioError` if a single atom does not match between the two selections. - ``False``: Will try to prepare a matching selection by dropping residues with non-matching atoms. See :func:`get_matching_atoms` for details. *force* - ``True``: Overwrite an existing output trajectory (default) - ``False``: simply return if the file already exists *quiet* - ``True``: suppress progress and logging for levels INFO and below. - ``False``: show all status messages and do not change the the logging level (default) .. Note:: If *kwargs* All other keyword arguments are passed on the trajectory :class:`~MDAnalysis.coordinates.base.Writer`; this allows manipulating/fixing trajectories on the fly (e.g. change the output format by changing the extension of *filename* and setting different parameters as described for the corresponding writer). :Returns: *filename* (either provided or auto-generated) .. _ClustalW: http://www.clustal.org/ .. _STAMP: http://www.compbio.dundee.ac.uk/manuals/stamp.4.2/ .. versionchanged:: 0.8 Added *kwargs* to be passed to the trajectory :class:`~MDAnalysis.coordinates.base.Writer` and *filename* is returned. .. versionchanged:: 0.10.0 Uses :func:`get_matching_atoms` to work with incomplete selections and new *strict* keyword. The new default is to be lenient whereas the old behavior was the equivalent of *strict* = ``True``. """ frames = traj.trajectory if quiet: # should be part of a try ... finally to guarantee restoring the log level logging.disable(logging.WARN) kwargs.setdefault('remarks', 'RMS fitted trajectory to reference') if filename is None: path, fn = os.path.split(frames.filename) filename = os.path.join(path, prefix + fn) _Writer = frames.Writer else: _Writer = frames.OtherWriter if os.path.exists(filename) and not force: logger.warn( "{0} already exists and will NOT be overwritten; use force=True if you want this" .format(filename)) return filename writer = _Writer(filename, **kwargs) del _Writer select = rms._process_selection(select) ref_atoms = reference.select_atoms(*select['reference']) traj_atoms = traj.select_atoms(*select['mobile']) natoms = traj_atoms.n_atoms ref_atoms, traj_atoms = get_matching_atoms(ref_atoms, traj_atoms, tol_mass=tol_mass, strict=strict) logger.info("RMS-fitting on {0:d} atoms.".format(len(ref_atoms))) if mass_weighted: # if performing a mass-weighted alignment/rmsd calculation weight = ref_atoms.masses / ref_atoms.masses.mean() else: weight = None # reference centre of mass system ref_com = ref_atoms.center_of_mass() ref_coordinates = ref_atoms.coordinates() - ref_com # allocate the array for selection atom coords traj_coordinates = traj_atoms.coordinates().copy() # RMSD timeseries nframes = len(frames) rmsd = np.zeros((nframes, )) # R: rotation matrix that aligns r-r_com, x~-x~com # (x~: selected coordinates, x: all coordinates) # Final transformed traj coordinates: x' = (x-x~_com)*R + ref_com rot = np.zeros(9, dtype=np.float64) # allocate space for calculation R = np.matrix(rot.reshape(3, 3)) percentage = ProgressMeter( nframes, interval=10, quiet=quiet, format="Fitted frame %(step)5d/%(numsteps)d [%(percentage)5.1f%%]\r") for k, ts in enumerate(frames): # shift coordinates for rotation fitting # selection is updated with the time frame x_com = traj_atoms.center_of_mass().astype(np.float32) traj_coordinates[:] = traj_atoms.coordinates() - x_com # Need to transpose coordinates such that the coordinate array is # 3xN instead of Nx3. Also qcp requires that the dtype be float64 # (I think we swapped the position of ref and traj in CalcRMSDRotationalMatrix # so that R acts **to the left** and can be broadcasted; we're saving # one transpose. [orbeckst]) rmsd[k] = qcp.CalcRMSDRotationalMatrix( ref_coordinates.T.astype(np.float64), traj_coordinates.T.astype(np.float64), natoms, rot, weight) R[:, :] = rot.reshape(3, 3) # Transform each atom in the trajectory (use inplace ops to avoid copying arrays) # (Marginally (~3%) faster than "ts.positions[:] = (ts.positions - x_com) * R + ref_com".) ts.positions -= x_com ts.positions[:] = ts.positions * R # R acts to the left & is broadcasted N times. ts.positions += ref_com writer.write(traj.atoms) # write whole input trajectory system percentage.echo(ts.frame) logger.info("Wrote %d RMS-fitted coordinate frames to file %r", frames.n_frames, filename) if rmsdfile is not None: np.savetxt(rmsdfile, rmsd) logger.info("Wrote RMSD timeseries to file %r", rmsdfile) if quiet: # should be part of a try ... finally to guarantee restoring the log level logging.disable(logging.NOTSET) return filename
def rotation_matrix(a, b, weights=None): r"""Returns the 3x3 rotation matrix for RMSD fitting coordinate sets *a* and *b*. The rotation matrix *R* transforms *a* to overlap with *b* (i.e. *b* is the reference structure): .. math:: \vec{b} = \bold{R} \dot \vec{a} Parameters ---------- a : array-like coordinates that are to be rotated ("mobile set"); array of N atoms of shape N*3 as generated by, e.g., :meth:`MDAnalysis.core.AtomGroup.AtomGroup.coordinates`. b : array-like reference coordinates; array of N atoms of shape N*3 as generated by, e.g., :meth:`MDAnalysis.core.AtomGroup.AtomGroup.coordinates`. weights : array-like (optional) array of floats of size N for doing weighted RMSD fitting (e.g. the masses of the atoms) Returns ------- R : ndarray rotation matrix rmsd : float RMSD between *a* and *b* before rotation ``(R, rmsd)`` rmsd and rotation matrix *R* Example ------- *R* can be used as an argument for :meth:`MDAnalysis.core.AtomGroup.AtomGroup.rotate` to generate a rotated selection, e.g. :: >>> R = rotation_matrix(A.select_atoms('backbone').coordinates(), >>> B.select_atoms('backbone').coordinates())[0] >>> A.atoms.rotate(R) >>> A.atoms.write("rotated.pdb") Notes ----- The function does *not* shift the centers of mass or geometry; this needs to be done by the user. See Also -------- :func:`rmsd` calculates the RMSD between *a* and *b*; for fitting a whole trajectory it is more efficient to use :func:`rms_fit_trj`. A complete fit of two structures can be done with :func:`alignto`. """ if weights is not None: # weights are constructed as relative to the mean weights = np.asarray(weights, dtype=np.float64) / np.mean(weights) a = np.asarray(a, dtype=np.float64) b = np.asarray(b, dtype=np.float64) N = b.shape[0] rot = np.zeros(9, dtype=np.float64) rmsd = qcp.CalcRMSDRotationalMatrix(a.T, b.T, N, rot, weights) return np.matrix(rot.reshape(3, 3)), rmsd
def rmsd(a, b, weights=None, center=False, superposition=False): r"""Returns RMSD between two coordinate sets `a` and `b`. `a` and `b` are arrays of the coordinates of N atoms of shape :math:`N times 3` as generated by, e.g., :meth:`MDAnalysis.core.groups.AtomGroup.positions`. Note ---- If you use trajectory data from simulations performed under **periodic boundary conditions** then you *must make your molecules whole* before performing RMSD calculations so that the centers of mass of the mobile and reference structure are properly superimposed. Parameters ---------- a : array_like coordinates to align to `b` b : array_like coordinates to align to (same shape as `a`) weights : array_like (optional) 1D array with weights, use to compute weighted average center : bool (optional) subtract center of geometry before calculation. With weights given compute weighted average as center. superposition : bool (optional) perform a rotational and translational superposition with the fast QCP algorithm [Theobald2005]_ before calculating the RMSD; implies ``center=True``. Returns ------- rmsd : float RMSD between `a` and `b` Notes ----- The RMSD :math:`\rho(t)` as a function of time is calculated as .. math:: \rho(t) = \sqrt{\frac{1}{N} \sum_{i=1}^N w_i \left(\mathbf{x}_i(t) - \mathbf{x}_i^{\text{ref}}\right)^2} It is the Euclidean distance in configuration space of the current configuration (possibly after optimal translation and rotation) from a reference configuration divided by :math:`1/\sqrt{N}` where :math:`N` is the number of coordinates. The weights :math:`w_i` are calculated from the input weights `weights` :math:`w'_i` as relative to the mean: .. math:: w_i = \frac{w'_i}{\langle w' \rangle} Example ------- >>> u = Universe(PSF,DCD) >>> bb = u.select_atoms('backbone') >>> A = bb.positions.copy() # coordinates of first frame >>> u.trajectory[-1] # forward to last frame >>> B = bb.positions.copy() # coordinates of last frame >>> rmsd(A, B, center=True) 3.9482355416565049 .. versionchanged: 0.8.1 *center* keyword added .. versionchanged: 0.14.0 *superposition* keyword added """ a = np.asarray(a, dtype=np.float64) b = np.asarray(b, dtype=np.float64) N = b.shape[0] if a.shape != b.shape: raise ValueError('a and b must have same shape') # superposition only works if structures are centered if center or superposition: # make copies (do not change the user data!) # weights=None is equivalent to all weights 1 a = a - np.average(a, axis=0, weights=weights) b = b - np.average(b, axis=0, weights=weights) if weights is not None: if len(weights) != len(a): raise ValueError('weights must have same length as a and b') # weights are constructed as relative to the mean weights = np.asarray(weights, dtype=np.float64) / np.mean(weights) if superposition: return qcp.CalcRMSDRotationalMatrix(a, b, N, None, weights) else: if weights is not None: return np.sqrt(np.sum(weights[:, np.newaxis] * ((a - b)**2)) / N) else: return np.sqrt(np.sum((a - b)**2) / N)
ref_coordinates = ref_atoms.coordinates() - ref_com # allocate the array for selection atom coords traj_coordinates = traj_atoms.coordinates().copy() # R: rotation matrix that aligns r-r_com, x~-x~com # (x~: selected coordinates, x: all coordinates) # Final transformed traj coordinates: x' = (x-x~_com)*R + ref_com for k, ts in enumerate(frames): # shift coordinates for rotation fitting # selection is updated with the time frame x_com = traj_atoms.center_of_mass() traj_coordinates[:] = traj_atoms.coordinates() - x_com R = np.zeros((9,), dtype=np.float64) # Need to transpose coordinates such that the coordinate array is # 3xN instead of Nx3. Also qcp requires that the dtype be float64 a = ref_coordinates.T.astype('float64') b = traj_coordinates.T.astype('float64') rmsd[k] = qcp.CalcRMSDRotationalMatrix(a, b, natoms, R, None) R = np.matrix(R.reshape(3, 3)) # Transform each atom in the trajectory (use inplace ops to avoid copying # arrays) ts._pos -= x_com ts._pos[:] = ts._pos * R # R acts to the left & is broadcasted N times. ts._pos += ref_com writer.write(traj.atoms) # write whole input trajectory system np.savetxt('rmsd.out', rmsd)
def rotation_matrix(a, b, weights=None): r"""Returns the 3x3 rotation matrix `R` for RMSD fitting coordinate sets `a` and `b`. The rotation matrix `R` transforms vector `a` to overlap with vector `b` (i.e., `b` is the reference structure): .. math:: \mathbf{b} = \mathsf{R} \cdot \mathbf{a} Parameters ---------- a : array_like coordinates that are to be rotated ("mobile set"); array of N atoms of shape N*3 as generated by, e.g., :attr:`MDAnalysis.core.groups.AtomGroup.positions`. b : array_like reference coordinates; array of N atoms of shape N*3 as generated by, e.g., :attr:`MDAnalysis.core.groups.AtomGroup.positions`. weights : array_like (optional) array of floats of size N for doing weighted RMSD fitting (e.g. the masses of the atoms) Returns ------- R : ndarray rotation matrix rmsd : float RMSD between `a` and `b` before rotation ``(R, rmsd)`` rmsd and rotation matrix *R* Example ------- `R` can be used as an argument for :meth:`MDAnalysis.core.groups.AtomGroup.rotate` to generate a rotated selection, e.g. :: >>> R = rotation_matrix(A.select_atoms('backbone').positions, >>> B.select_atoms('backbone').positions)[0] >>> A.atoms.rotate(R) >>> A.atoms.write("rotated.pdb") Notes ----- The function does *not* shift the centers of mass or geometry; this needs to be done by the user. See Also -------- MDAnalysis.analysis.rms.rmsd: Calculates the RMSD between *a* and *b*. alignto: A complete fit of two structures. AlignTraj: Fit a whole trajectory. """ a = np.asarray(a, dtype=np.float64) b = np.asarray(b, dtype=np.float64) if a.shape != b.shape: raise ValueError("'a' and 'b' must have same shape") N = b.shape[0] if weights is not None: # qcp does NOT divide weights relative to the mean weights = np.asarray(weights, dtype=np.float64) / np.mean(weights) rot = np.zeros(9, dtype=np.float64) # Need to transpose coordinates such that the coordinate array is # 3xN instead of Nx3. Also qcp requires that the dtype be float64 # (I think we swapped the position of ref and traj in CalcRMSDRotationalMatrix # so that R acts **to the left** and can be broadcasted; we're saving # one transpose. [orbeckst]) rmsd = qcp.CalcRMSDRotationalMatrix(a, b, N, rot, weights) return np.matrix(rot.reshape(3, 3)), rmsd