LCOV - code coverage report
Current view: top level - xsprint - Xsprint.cpp (source / functions) Hit Total Coverage
Test: plumed test coverage Lines: 100 104 96.2 %
Date: 2026-11-05 10:19:04 Functions: 3 4 75.0 %

          Line data    Source code
       1             : /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
       2             :    Copyright (c) 2026 Rangsiman Ketkaew
       3             : 
       4             :    The xsprint module is free software: you can redistribute it and/or modify
       5             :    it under the terms of the GNU Lesser General Public License as published by
       6             :    the Free Software Foundation, either version 3 of the License, or
       7             :    (at your option) any later version.
       8             : 
       9             :    The xsprint module is distributed in the hope that it will be useful,
      10             :    but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :    GNU Lesser General Public License for more details.
      13             : 
      14             :    You should have received a copy of the GNU Lesser General Public License
      15             :    along with plumed.  If not, see <http://www.gnu.org/licenses/>.
      16             : +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */
      17             : #include "core/ActionShortcut.h"
      18             : #include "core/ActionRegister.h"
      19             : #include "core/ActionWithValue.h"
      20             : #include "core/PlumedMain.h"
      21             : #include "core/ActionSet.h"
      22             : 
      23             : //+PLUMEDOC MATRIXF XSPRINT
      24             : /*
      25             : Calculate eXtended SPRINT (xSPRINT) topological variables from adjacency matrices over multiple shells.
      26             : 
      27             : The xSPRINT coordinates, introduced in \cite ketkaew2022, extend the standard [SPRINT](SPRINT.md) coordinates
      28             : by incorporating structural information from multiple concentric spherical shells around each atom.
      29             : For a system divided into $N$ shells, the xSPRINT coordinate of atom $i$ is defined as:
      30             : 
      31             : $$
      32             : S_i^x = \begin{cases}
      33             :   S_i, & \text{if } r_{ij} < r_0 \\
      34             :   \frac{1}{\omega} \sum_{n=1}^{N} r_n\, S_i^{(n)}, & \text{otherwise}
      35             : \end{cases}
      36             : $$
      37             : 
      38             : where $S_i^{(n)}$ is the SPRINT coordinate computed from the adjacency matrix of the $n$-th shell,
      39             : $r_n = n\,r_0$ is the shell radius, $r_0$ is the user-defined cutoff for the first shell, and
      40             : $\omega = \left(\sum_{n=1}^{N} r_n\right)^2$ is a normalization weight.  Adjacency matrix elements
      41             : from inner shells are zeroed out before computing each shell's SPRINT to avoid redundancy.
      42             : 
      43             : When `NSHELLS=1` the xSPRINT coordinate reduces to the standard SPRINT coordinate.
      44             : 
      45             : The following example computes the xSPRINT coordinates for a 7-atom cluster using 3 shells
      46             : with a shell radius of 0.1:
      47             : 
      48             : ```plumed
      49             : xs: XSPRINT GROUP=1-7 SWITCH={RATIONAL R_0=0.1} R0=0.1 NSHELLS=3
      50             : PRINT ARG=xs.* FILE=colvar
      51             : ```
      52             : 
      53             : Note that `R0` defines the xSPRINT shell radius ($r_0$ in the equation) and is independent
      54             : of `R_0` inside `SWITCH`, which controls the shape of the switching function.  For shell $n$,
      55             : the switching function `R_0` is automatically set to $n \times$ `R0`.
      56             : 
      57             : This example computes xSPRINT with 2 shells for two groups of atoms:
      58             : 
      59             : ```plumed
      60             : xs: XSPRINT ...
      61             :   GROUP1=1-7 GROUP2=8-14
      62             :   SWITCH11={RATIONAL R_0=2.6 NN=6 MM=12}
      63             :   SWITCH12={RATIONAL R_0=2.2 NN=6 MM=12}
      64             :   SWITCH22={RATIONAL R_0=2.2 NN=6 MM=12}
      65             :   R0=2.0
      66             :   NSHELLS=2
      67             : ...
      68             : 
      69             : PRINT ARG=xs.* FILE=colvar
      70             : ```
      71             : 
      72             : You can also provide a pre-built matrix (for a single-shell case this is equivalent to SPRINT):
      73             : 
      74             : ```plumed
      75             : b: BRIDGE_MATRIX GROUP=1-7 BRIDGING_ATOMS=8-100 SWITCH={RATIONAL R_0=0.2}
      76             : xs: XSPRINT MATRIX=b NSHELLS=1
      77             : PRINT ARG=xs.* FILE=colvar
      78             : ```
      79             : 
      80             : */
      81             : //+ENDPLUMEDOC
      82             : 
      83             : namespace PLMD {
      84             : namespace xsprint {
      85             : 
      86             : class Xsprint : public ActionShortcut {
      87             : public:
      88             :   static void registerKeywords(Keywords& keys);
      89             :   explicit Xsprint(const ActionOptions&);
      90             : };
      91             : 
      92             : // Function to replace all R_0=<value> entries in a string with R_0=<newval>
      93           3 : static std::string replaceR0(const std::string& input, double newval) {
      94           3 :   std::string result = input;
      95           3 :   std::string needle = "R_0=";
      96             :   std::string newvalstr;
      97           3 :   Tools::convert(newval, newvalstr);
      98             :   size_t pos = 0;
      99           6 :   while((pos = result.find(needle, pos)) != std::string::npos) {
     100           3 :     size_t valstart = pos + needle.length();
     101           3 :     size_t valend = result.find_first_of(" \t},", valstart);
     102           3 :     if(valend == std::string::npos) {
     103             :       valend = result.length();
     104             :     }
     105           3 :     result.replace(valstart, valend - valstart, newvalstr);
     106           3 :     pos = valstart + newvalstr.length();
     107             :   }
     108           3 :   return result;
     109             : }
     110             : 
     111             : PLUMED_REGISTER_ACTION(Xsprint,"XSPRINT")
     112             : 
     113           8 : void Xsprint::registerKeywords(Keywords& keys) {
     114           8 :   ActionShortcut::registerKeywords( keys );
     115           8 :   keys.add("optional","MATRIX","the matrix that you would like to perform xSPRINT on (used only when NSHELLS=1)");
     116           8 :   keys.add("numbered","GROUP","specifies the list of atoms that should be assumed indistinguishable");
     117           8 :   keys.add("numbered","SWITCH","specify the switching function to use between two sets of indistinguishable atoms");
     118           8 :   keys.add("compulsory","NSHELLS","1","the number of concentric shells to use for the xSPRINT calculation");
     119           8 :   keys.add("optional","R0","the radius of the first shell for xSPRINT (shell n has radius n*R0). Required when NSHELLS>1. This is independent of the R_0 in SWITCH which controls the switching function shape.");
     120           8 :   keys.needsAction("CONTACT_MATRIX");
     121           8 :   keys.needsAction("SPRINT");
     122           8 :   keys.needsAction("CUSTOM");
     123           8 :   keys.needsAction("SELECT_COMPONENTS");
     124           8 :   keys.needsAction("SORT");
     125           8 :   keys.needsAction("COMBINE");
     126          16 :   keys.addOutputComponent("coord","default","scalar","the xsprint coordinates");
     127           8 :   keys.addDOI("10.1021/acs.jpclett.1c04004");
     128           8 : }
     129             : 
     130           4 : Xsprint::Xsprint(const ActionOptions& ao):
     131             :   Action(ao),
     132           4 :   ActionShortcut(ao) {
     133           4 :   const std::string label = getShortcutLabel();
     134             : 
     135           4 :   unsigned nshells = 1;
     136           4 :   parse("NSHELLS", nshells);
     137           4 :   if( nshells<1 ) {
     138           2 :     error("NSHELLS should be at least 1");
     139             :   }
     140             :   std::string matinp;
     141           6 :   parse("MATRIX", matinp);
     142           3 :   if( !matinp.empty() && nshells > 1 ) {
     143           0 :     error("MATRIX keyword can only be used with NSHELLS=1");
     144             :   }
     145           3 :   double r0 = 0.0;
     146           3 :   parse("R0", r0);
     147           3 :   if( nshells > 1 && r0 <= 0.0 ) {
     148           0 :     plumed_merror("R0 keyword is required and must be positive when NSHELLS > 1");
     149             :   }
     150             : 
     151             :   // NSHELLS == 1 : use existing SPRINT
     152           3 :   if( nshells == 1 ) {
     153           2 :     if( !matinp.empty() ) {
     154           2 :       readInputLine( label + "_sprint: SPRINT MATRIX=" + matinp );
     155             :     } else {
     156           2 :       readInputLine( label + "_sprint: SPRINT " + convertInputLineToString() );
     157             :     }
     158             :     // Forward SPRINT's coord-N outputs as our own coord-N outputs
     159           2 :     for(unsigned i = 0;; ++i) {
     160             :       std::string inum;
     161          30 :       Tools::convert( i, inum );
     162          60 :       ActionWithValue* av = plumed.getActionSet().selectWithLabel<ActionWithValue*>(
     163          30 :                               label + "_sprint_coord-" + inum );
     164          30 :       if( !av ) {
     165             :         break;
     166             :       }
     167          84 :       readInputLine( label + "_coord-" + inum + ": COMBINE ARG=" +
     168          56 :                      label + "_sprint_coord-" + inum + " PERIODIC=NO" );
     169          28 :     }
     170             :     return;
     171             :   }
     172             : 
     173             :   // NSHELLS > 1 : compute xSPRINT using SPRINT per shell
     174           1 :   std::string baseargs = convertInputLineToString();
     175             : 
     176             :   // Shell 1: create CONTACT_MATRIX with R_0=r0 and compute SPRINT
     177           1 :   std::string shell1args = replaceR0(baseargs, r0);
     178           1 :   readInputLine( label + "_shell1_mat: CONTACT_MATRIX " + shell1args );
     179           2 :   readInputLine( label + "_shell1_sprint: SPRINT MATRIX=" + label + "_shell1_mat" );
     180             : 
     181             :   // Determine group sizes from shell-1 matrix (this is needed for final sorting)
     182           1 :   std::string mat1 = label + "_shell1_mat";
     183           1 :   std::vector<unsigned> nin_group;
     184           1 :   for(unsigned i = 1;; ++i) {
     185             :     std::string inum;
     186           1 :     Tools::convert( i, inum );
     187           1 :     ActionWithValue* av = plumed.getActionSet().selectWithLabel<ActionWithValue*>( mat1 + inum + inum );
     188           1 :     if( !av ) {
     189             :       break;
     190             :     }
     191           0 :     nin_group.push_back( (av->copyOutput(0))->getShape()[0] );
     192           0 :   }
     193           1 :   if( nin_group.empty() ) {
     194           1 :     ActionWithValue* av = plumed.getActionSet().selectWithLabel<ActionWithValue*>( mat1 );
     195           1 :     nin_group.push_back( (av->copyOutput(0))->getShape()[0] );
     196             :   }
     197             : 
     198             :   // For shells 2..N: create CONTACT_MATRIX with R_0=s*r0,
     199             :   // subtract inner shell, compute SPRINT, respectively
     200           3 :   for(unsigned s = 2; s <= nshells; ++s) {
     201             :     std::string snum, sprev;
     202           2 :     Tools::convert( s, snum );
     203           2 :     Tools::convert( s - 1, sprev );
     204             : 
     205           2 :     double shell_r0 = static_cast<double>(s) * r0;
     206           2 :     std::string shellargs = replaceR0(baseargs, shell_r0);
     207           4 :     readInputLine( label + "_shell" + snum + "_mat: CONTACT_MATRIX " + shellargs );
     208             : 
     209             :     // Subtract inner-shell matrix to isolate contacts in this shell only
     210           6 :     readInputLine( label + "_shell" + snum + "_diff: CUSTOM ARG=" +
     211           4 :                    label + "_shell" + snum + "_mat," +
     212           2 :                    label + "_shell" + sprev + "_mat FUNC=x-y PERIODIC=NO" );
     213             : 
     214           6 :     readInputLine( label + "_shell" + snum + "_sprint: SPRINT MATRIX=" +
     215           4 :                    label + "_shell" + snum + "_diff" );
     216             :   }
     217             : 
     218             :   // Combine per-shell SPRINT vectors with weights r_n / omega
     219             :   // where r_n = n*r0, omega = (sum_{n=1}^{N} r_n)^2
     220           1 :   double sum_rn = r0 * static_cast<double>(nshells) * static_cast<double>(nshells + 1) / 2.0;
     221           1 :   double omega = sum_rn * sum_rn;
     222             : 
     223             :   std::string combine_args, coefficients;
     224           4 :   for(unsigned s = 1; s <= nshells; ++s) {
     225             :     std::string snum;
     226           3 :     Tools::convert( s, snum );
     227           3 :     if( s > 1 ) {
     228             :       combine_args += ",";
     229             :       coefficients += ",";
     230             :     }
     231           6 :     combine_args += label + "_shell" + snum + "_sprint_sp";
     232             :     std::string wstr;
     233           3 :     Tools::convert( static_cast<double>(s) * r0 / omega, wstr );
     234             :     coefficients += wstr;
     235             :   }
     236             : 
     237           2 :   readInputLine( label + "_xsp: COMBINE ARG=" + combine_args +
     238           1 :                  " COEFFICIENTS=" + coefficients + " PERIODIC=NO" );
     239             : 
     240             :   // Sort xSPRINT coordinates per group
     241           1 :   unsigned k = 0, kk = 0;
     242           2 :   for(unsigned j = 0; j < nin_group.size(); ++j) {
     243             :     std::string jnum, knum;
     244           1 :     Tools::convert( j+1, jnum );
     245           1 :     Tools::convert( k+1, knum );
     246             :     k++;
     247           3 :     std::string sort_act = label + "_selection" + jnum + ": SELECT_COMPONENTS ARG=" + label + "_xsp COMPONENTS=" + knum;
     248          14 :     for(unsigned n = 1; n < nin_group[j]; ++n) {
     249          13 :       Tools::convert( k+1, knum );
     250          13 :       sort_act += "," + knum;
     251             :       k++;
     252             :     }
     253           1 :     readInputLine( sort_act );
     254           3 :     readInputLine( label + jnum + ": SORT ARG=" + label + "_selection" + jnum );
     255          15 :     for(unsigned n = 0; n < nin_group[j]; ++n) {
     256             :       std::string nnum;
     257          14 :       Tools::convert( kk, knum );
     258          14 :       Tools::convert( n+1, nnum );
     259          14 :       kk++;
     260          70 :       readInputLine( label + "_coord-" + knum + ": COMBINE ARG=" + label + jnum + "." + nnum + " PERIODIC=NO" );
     261             :     }
     262             :   }
     263          16 : }
     264             : 
     265             : }
     266             : }

Generated by: LCOV version 1.16