LCOV - code coverage report
Current view: top level - dimred - ClassicalMultiDimensionalScaling.cpp (source / functions) Hit Total Coverage
Test: plumed test coverage Lines: 54 56 96.4 %
Date: 2025-12-04 11:19:34 Functions: 2 3 66.7 %

          Line data    Source code
       1             : /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
       2             :    Copyright (c) 2015-2020 The plumed team
       3             :    (see the PEOPLE file at the root of the distribution for a list of names)
       4             : 
       5             :    See http://www.plumed.org for more information.
       6             : 
       7             :    This file is part of plumed, version 2.
       8             : 
       9             :    plumed is free software: you can redistribute it and/or modify
      10             :    it under the terms of the GNU Lesser General Public License as published by
      11             :    the Free Software Foundation, either version 3 of the License, or
      12             :    (at your option) any later version.
      13             : 
      14             :    plumed is distributed in the hope that it will be useful,
      15             :    but WITHOUT ANY WARRANTY; without even the implied warranty of
      16             :    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17             :    GNU Lesser General Public License for more details.
      18             : 
      19             :    You should have received a copy of the GNU Lesser General Public License
      20             :    along with plumed.  If not, see <http://www.gnu.org/licenses/>.
      21             : +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */
      22             : #include "core/ActionShortcut.h"
      23             : #include "core/ActionPilot.h"
      24             : #include "core/ActionWithValue.h"
      25             : #include "core/ActionRegister.h"
      26             : #include "core/PlumedMain.h"
      27             : #include "core/ActionSet.h"
      28             : 
      29             : //+PLUMEDOC DIMRED CLASSICAL_MDS
      30             : /*
      31             : Create a low-dimensional projection of a trajectory using the classical multidimensional
      32             :  scaling algorithm.
      33             : 
      34             : Multidimensional scaling (MDS) is similar to what is done when you make a map. You start with distances
      35             : between London, Belfast, Paris and Dublin and then you try to arrange points on a piece of paper so that the (suitably scaled)
      36             : distances between the points in your map representing each of those cities are related to the true distances between the cities.
      37             : Stating this more mathematically MDS endeavors to find an [isometry](http://en.wikipedia.org/wiki/Isometry)
      38             : between points distributed in a high-dimensional space and a set of points distributed in a low-dimensional plane.
      39             : In other words, if we have $M$ $D$-dimensional points, $\mathbf{X}$,
      40             : and we can calculate dissimilarities between pairs them, $D_{ij}$, we can, with an MDS calculation, try to create $M$ projections,
      41             : $\mathbf{x}$, of the high dimensionality points in a $d$-dimensional linear space by trying to arrange the projections so that the
      42             : Euclidean distances between pairs of them, $d_{ij}$, resemble the dissimilarities between the high dimensional points.  In short we minimize:
      43             : 
      44             : $$
      45             : \chi^2 = \sum_{i \ne j} \left( D_{ij} - d_{ij} \right)^2
      46             : $$
      47             : 
      48             : where $D_{ij}$ is the distance between point $X^{i}$ and point $X^{j}$ and $d_{ij}$ is the distance between the projection
      49             : of $X^{i}$, $x^i$, and the projection of $X^{j}$, $x^j$.  A tutorial on this approach can be used to analyze simulations
      50             : can be found in [this tutorial](https://www.plumed-tutorials.org/lessons/21/006/data/DIMENSIONALITY.html) and in the following [short video](https://www.youtube.com/watch?v=ofC2qz0_9_A&feature=youtu.be).
      51             : 
      52             : ## Examples
      53             : 
      54             : The following command instructs plumed to construct a classical multidimensional scaling projection of a trajectory.
      55             : The RMSD distance between atoms 1-256 have moved is used to measure the distances in the high-dimensional space.
      56             : 
      57             : ```plumed
      58             : ff: COLLECT_FRAMES ATOMS=1-256
      59             : mds: CLASSICAL_MDS ARG=ff NLOW_DIM=2
      60             : weights: CUSTOM ARG=ff.logweights FUNC=exp(x) PERIODIC=NO
      61             : DUMPPDB ATOM_INDICES=1-256 ATOMS=ff_data ARG=mds,weights FILE=embed.pdb
      62             : ```
      63             : 
      64             : By contrast the following input instructs PLUMED to calculate the distances between atoms by taking the differences in five torsional angles.
      65             : The MDS algorithm is then used to arrange a set of points in a low dimensional space in a way that reproduces these differences.
      66             : 
      67             : ```plumed
      68             : phi1: TORSION ATOMS=1,2,3,4
      69             : phi2: TORSION ATOMS=5,6,7,8
      70             : phi3: TORSION ATOMS=9,10,11,12
      71             : phi4: TORSION ATOMS=13,14,15,16
      72             : phi5: TORSION ATOMS=17,18,19,20
      73             : 
      74             : angles: COLLECT_FRAMES ARG=phi1,phi2,phi3,phi4,phi5
      75             : mds: CLASSICAL_MDS ARG=angles NLOW_DIM=2
      76             : weights: CUSTOM ARG=angles.logweights FUNC=exp(x) PERIODIC=NO
      77             : DUMPVECTOR ARG=mds,weights FILE=list_embed
      78             : ```
      79             : 
      80             : The following section is for people who are interested in how this method works in detail. A solid understanding of this material is
      81             : not necessary to use MDS.
      82             : 
      83             : ## Method of optimization
      84             : 
      85             : The stress function can be minimized using the conjugate gradients or steepest descent optimization algorithms that are implemented in [ARRANGE_POINTS](ARRANGE_POINTS.md).
      86             : However, it is more common to do this minimization using a technique known as classical scaling.  Classical scaling works by
      87             : recognizing that each of the distances $D_{ij}$ in the above sum can be written as:
      88             : 
      89             : $$
      90             : D_{ij}^2 = \sum_{\alpha} (X^i_\alpha - X^j_\alpha)^2 = \sum_\alpha (X^i_\alpha)^2 + (X^j_\alpha)^2 - 2X^i_\alpha X^j_\alpha
      91             : $$
      92             : 
      93             : We can use this expression and matrix algebra to calculate multiple distances at once.  For instance if we have three points,
      94             : $\mathbf{X}$, we can write distances between them as:
      95             : 
      96             : $$
      97             : \begin{aligned}
      98             : D^2(\mathbf{X}) &= \left[ \begin{array}{ccc}
      99             : 0 & d_{12}^2 & d_{13}^2 \\
     100             : d_{12}^2 & 0 & d_{23}^2 \\
     101             : d_{13}^2 & d_{23}^2 & 0
     102             : \end{array}\right] \\
     103             : &=
     104             : \sum_\alpha \left[ \begin{array}{ccc}
     105             : (X^1_\alpha)^2 & (X^1_\alpha)^2 & (X^1_\alpha)^2 \\
     106             : (X^2_\alpha)^2 & (X^2_\alpha)^2 & (X^2_\alpha)^2 \\
     107             : (X^3_\alpha)^2 & (X^3_\alpha)^2 & (X^3_\alpha)^2 \\
     108             : \end{array}\right]
     109             :  + \sum_\alpha \left[ \begin{array}{ccc}
     110             : (X^1_\alpha)^2 & (X^2_\alpha)^2 & (X^3_\alpha)^2 \\
     111             : (X^1_\alpha)^2 & (X^2_\alpha)^2 & (X^3_\alpha)^2 \\
     112             : (X^1_\alpha)^2 & (X^2_\alpha)^2 & (X^3_\alpha)^2 \\
     113             : \end{array}\right]
     114             : - 2 \sum_\alpha \left[ \begin{array}{ccc}
     115             : X^1_\alpha X^1_\alpha & X^1_\alpha X^2_\alpha & X^1_\alpha X^3_\alpha \\
     116             : X^2_\alpha X^1_\alpha & X^2_\alpha X^2_\alpha & X^2_\alpha X^3_\alpha \\
     117             : X^1_\alpha X^3_\alpha & X^3_\alpha X^2_\alpha & X^3_\alpha X^3_\alpha
     118             : \end{array}\right] \nonumber \\
     119             : &= \mathbf{c 1^T} + \mathbf{1 c^T} - 2 \sum_\alpha \mathbf{x}_a \mathbf{x}^T_a =  \mathbf{c 1^T} + \mathbf{1 c^T} - 2\mathbf{X X^T}
     120             : \end{aligned}
     121             : $$
     122             : 
     123             : This last equation can be extended to situations when we have more than three points.  In it $\mathbf{X}$ is a matrix that has
     124             : one high-dimensional point on each of its rows and $\mathbf{X^T}$ is its transpose.  $\mathbf{1}$ is an $M \times 1$ vector
     125             : of ones and $\mathbf{c}$ is a vector with components given by:
     126             : 
     127             : $$
     128             : c_i = \sum_\alpha (x_\alpha^i)^2
     129             : $$
     130             : 
     131             : These quantities are the diagonal elements of $\mathbf{X X^T}$, which is a dot product or Gram Matrix that contains the
     132             : dot product of the vector $X_i$ with the vector $X_j$ in element $i,j$.
     133             : 
     134             : In classical scaling we introduce a centering matrix $\mathbf{J}$ that is given by:
     135             : 
     136             : $$
     137             : \mathbf{J} = \mathbf{I} - \frac{1}{M} \mathbf{11^T}
     138             : $$
     139             : 
     140             : where $\mathbf{I}$ is the identity.  Multiplying the equations above from the front and back by this matrix and a factor of a $-\frac{1}{2}$ gives:
     141             : 
     142             : $$
     143             : \begin{aligned}
     144             :  -\frac{1}{2} \mathbf{J} \mathbf{D}^2(\mathbf{X}) \mathbf{J} &= -\frac{1}{2}\mathbf{J}( \mathbf{c 1^T} + \mathbf{1 c^T} - 2\mathbf{X X^T})\mathbf{J} \\
     145             :  &= -\frac{1}{2}\mathbf{J c 1^T J} - \frac{1}{2} \mathbf{J 1 c^T J} + \frac{1}{2} \mathbf{J}(2\mathbf{X X^T})\mathbf{J} \\
     146             :  &= \mathbf{ J X X^T J } = \mathbf{X X^T } \label{eqn:scaling}
     147             : \end{aligned}
     148             : $$
     149             : 
     150             : The fist two terms in this expression disappear because $\mathbf{1^T J}=\mathbf{J 1} =\mathbf{0}$, where $\mathbf{0}$
     151             : is a matrix containing all zeros.  In the final step meanwhile we use the fact that the matrix of squared distances will not
     152             : change when we translate all the points.  We can thus assume that the mean value, $\mu$, for each of the components, $\alpha$:
     153             : 
     154             : $$
     155             : \mu_\alpha = \frac{1}{M} \sum_{i=1}^N \mathbf{X}^i_\alpha
     156             : $$
     157             : 
     158             : is equal to 0 so the columns of $\mathbf{X}$ add up to 0.  This in turn means that each of the columns of
     159             : $\mathbf{X X^T}$ adds up to zero, which is what allows us to write $\mathbf{ J X X^T J } = \mathbf{X X^T }$.
     160             : 
     161             : The matrix of squared distances is symmetric and positive-definite we can thus use the spectral decomposition to decompose it as:
     162             : 
     163             : $$
     164             : \Phi= \mathbf{V} \Lambda \mathbf{V}^T
     165             : $$
     166             : 
     167             : Furthermore, because the matrix we are diagonalizing, $\mathbf{X X^T}$, is the product of a matrix and its transpose
     168             : we can use this decomposition to write:
     169             : 
     170             : $$
     171             : \mathbf{X} =\mathbf{V} \Lambda^\frac{1}{2}
     172             : $$
     173             : 
     174             : Much as in PCA there are generally a small number of large eigenvalues in $\Lambda$ and many small eigenvalues.
     175             : We can safely use only the large eigenvalues and their corresponding eigenvectors to express the relationship between
     176             : the coordinates $\mathbf{X}$.  This gives us our set of low-dimensional projections.
     177             : 
     178             : This derivation makes a number of assumptions about the how the low dimensional points should best be arranged to minimize
     179             : the stress. If you use an interactive optimization algorithm such as SMACOF you may thus be able to find a better
     180             : (lower-stress) projection of the points.  For more details on the assumptions made
     181             : see [this website](http://quest4rigor.com/tag/multidimensional-scaling/).
     182             : */
     183             : //+ENDPLUMEDOC
     184             : 
     185             : namespace PLMD {
     186             : namespace dimred {
     187             : 
     188             : class ClassicalMultiDimensionalScaling : public ActionShortcut {
     189             : public:
     190             :   static void registerKeywords( Keywords& keys );
     191             :   explicit ClassicalMultiDimensionalScaling( const ActionOptions& ao );
     192             : };
     193             : 
     194             : PLUMED_REGISTER_ACTION(ClassicalMultiDimensionalScaling,"CLASSICAL_MDS")
     195             : 
     196          13 : void ClassicalMultiDimensionalScaling::registerKeywords( Keywords& keys ) {
     197          13 :   ActionShortcut::registerKeywords( keys );
     198          13 :   keys.add("compulsory","ARG","the arguments that you would like to make the histogram for");
     199          13 :   keys.add("compulsory","NLOW_DIM","number of low-dimensional coordinates required");
     200          26 :   keys.setValueDescription("matrix","the low dimensional projections for the input data points");
     201          13 :   keys.needsAction("TRANSPOSE");
     202          13 :   keys.needsAction("DISSIMILARITIES");
     203          13 :   keys.needsAction("MATRIX_VECTOR_PRODUCT");
     204          13 :   keys.needsAction("VSTACK");
     205          13 :   keys.needsAction("SUM");
     206          13 :   keys.needsAction("CUSTOM");
     207          13 :   keys.needsAction("OUTER_PRODUCT");
     208          13 :   keys.needsAction("DIAGONALIZE");
     209          13 : }
     210             : 
     211           7 : ClassicalMultiDimensionalScaling::ClassicalMultiDimensionalScaling( const ActionOptions& ao):
     212             :   Action(ao),
     213           7 :   ActionShortcut(ao) {
     214             :   // Find the argument name
     215             :   std::string argn;
     216           7 :   parse("ARG",argn);
     217           7 :   std::string dissimilarities="";
     218           7 :   ActionShortcut* as = plumed.getActionSet().getShortcutActionWithLabel( argn );
     219           7 :   if( !as ) {
     220           0 :     error("found no action with name " + argn );
     221             :   }
     222           7 :   if( as->getName()!="COLLECT_FRAMES" ) {
     223           1 :     if( as->getName().find("LANDMARK_SELECT")==std::string::npos ) {
     224           0 :       error("found no COLLECT_FRAMES or LANDMARK_SELECT action with label " + argn );
     225             :     } else {
     226           1 :       ActionWithValue* dissims = plumed.getActionSet().selectWithLabel<ActionWithValue*>( argn + "_sqrdissims");
     227           1 :       if( dissims ) {
     228           2 :         dissimilarities = argn + "_sqrdissims";
     229             :       }
     230             :     }
     231             :   }
     232           7 :   if( dissimilarities.length()==0 ) {
     233           6 :     dissimilarities = getShortcutLabel() + "_mat";
     234             :     // Transpose matrix of stored data values
     235          12 :     readInputLine( argn + "_dataT: TRANSPOSE ARG=" + argn + "_data");
     236             :     // Calculate the dissimilarity matrix
     237          12 :     readInputLine( getShortcutLabel() + "_mat: DISSIMILARITIES SQUARED ARG=" + argn + "_data," + argn + "_dataT");
     238             :   }
     239             :   // Center the matrix
     240             :   // Step 1: calculate the sum of the rows and duplicate them into a matrix
     241          14 :   readInputLine( getShortcutLabel() + "_rsums: MATRIX_VECTOR_PRODUCT ARG=" + dissimilarities + "," + argn + "_ones" );
     242          14 :   readInputLine( getShortcutLabel() + "_nones: SUM ARG=" + argn + "_ones PERIODIC=NO");
     243          14 :   readInputLine( getShortcutLabel() + "_rsumsn: CUSTOM ARG=" + getShortcutLabel() + "_rsums," + getShortcutLabel() + "_nones FUNC=x/y PERIODIC=NO");
     244          14 :   readInputLine( getShortcutLabel() + "_rsummat: OUTER_PRODUCT ARG=" + getShortcutLabel() + "_rsumsn," + argn + "_ones");
     245             :   // Step 2: Multiply matrix by -0.5 and subtract row sums
     246          14 :   readInputLine( getShortcutLabel() + "_int: CUSTOM ARG=" + getShortcutLabel() + "_rsummat," + dissimilarities + " FUNC=-0.5*y+0.5*x PERIODIC=NO");
     247             :   // Step 3: Calculate column sums for new matrix and duplicate them into a matrix
     248          14 :   readInputLine( getShortcutLabel() + "_intT: TRANSPOSE ARG=" + getShortcutLabel() + "_int");
     249          14 :   readInputLine( getShortcutLabel() + "_csums: MATRIX_VECTOR_PRODUCT ARG=" + getShortcutLabel() + "_intT," + argn + "_ones" );
     250          14 :   readInputLine( getShortcutLabel() + "_csumsn: CUSTOM ARG=" + getShortcutLabel() + "_csums," + getShortcutLabel() + "_nones FUNC=x/y PERIODIC=NO");
     251          14 :   readInputLine( getShortcutLabel() + "_csummat: OUTER_PRODUCT ARG=" + getShortcutLabel() + "_csumsn," + argn + "_ones");
     252             :   // Step 4: subtract the column sums
     253          14 :   readInputLine( getShortcutLabel() + "_cmat: CUSTOM ARG=" + getShortcutLabel() + "_csummat," + getShortcutLabel() + "_intT FUNC=y-x PERIODIC=NO");
     254             :   // And generate the multidimensional scaling projection
     255             :   unsigned ndim;
     256           7 :   parse("NLOW_DIM",ndim);
     257           7 :   std::string vecstr="1";
     258          13 :   for(unsigned i=1; i<ndim; ++i) {
     259             :     std::string num;
     260           6 :     Tools::convert( i+1, num );
     261          12 :     vecstr += "," + num;
     262             :   }
     263          14 :   readInputLine( getShortcutLabel() + "_eig: DIAGONALIZE ARG=" + getShortcutLabel() + "_cmat VECTORS=" + vecstr );
     264          20 :   for(unsigned i=0; i<ndim; ++i) {
     265             :     std::string num;
     266          13 :     Tools::convert( i+1, num );
     267          26 :     readInputLine( getShortcutLabel() + "-" +  num + ": CUSTOM ARG=" + getShortcutLabel() + "_eig.vecs-" + num + "," + getShortcutLabel() + "_eig.vals-" + num + " FUNC=sqrt(y)*x PERIODIC=NO");
     268             :   }
     269           7 :   std::string eigvec_args = " ARG=" + getShortcutLabel() + "-1";
     270             :   // The final output is a stack of all the low dimensional coordinates
     271          13 :   for(unsigned i=1; i<ndim; ++i) {
     272             :     std::string num;
     273           6 :     Tools::convert( i+1, num );
     274          12 :     eigvec_args += "," + getShortcutLabel() + "-" + num;
     275             :   }
     276          14 :   readInputLine( getShortcutLabel() + ": VSTACK" + eigvec_args );
     277           7 : }
     278             : 
     279             : }
     280             : }

Generated by: LCOV version 1.16