Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added new method for links to switching function documentation #1145

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/adjmat/BridgeMatrix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,13 @@ void BridgeMatrix::registerKeywords( Keywords& keys ) {
keys.add("atoms","BRIDGING_ATOMS","The list of atoms that can form the bridge between the two interesting parts "
"of the structure.");
keys.add("optional","SWITCH","The parameters of the two switchingfunction in the above formula");
keys.linkActionInDocs("SWITCH","LESS_THAN");
keys.add("optional","SWITCHA","The switchingfunction on the distance between bridging atoms and the atoms in "
"group A");
keys.linkActionInDocs("SWITCHA","LESS_THAN");
keys.add("optional","SWITCHB","The switchingfunction on the distance between the bridging atoms and the atoms in "
"group B");
keys.linkActionInDocs("SWITCHB","LESS_THAN");
}

BridgeMatrix::BridgeMatrix(const ActionOptions&ao):
Expand Down
1 change: 1 addition & 0 deletions src/adjmat/ContactMatrix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ void ContactMatrix::registerKeywords( Keywords& keys ) {
keys.add("optional","SWITCH","This keyword is used if you want to employ an alternative to the continuous swiching function defined above. "
"The following provides information on the \\ref switchingfunction that are available. "
"When this keyword is present you no longer need the NN, MM, D_0 and R_0 keywords.");
keys.linkActionInDocs("SWITCH","LESS_THAN");
}

ContactMatrix::ContactMatrix( const ActionOptions& ao ):
Expand Down
1 change: 1 addition & 0 deletions src/adjmat/ContactMatrixShortcut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ void ContactMatrixShortcut::registerKeywords(Keywords& keys) {
keys.add("compulsory","D_0","0.0","The d_0 parameter of the switching function");
keys.add("compulsory","R_0","The r_0 parameter of the switching function");
keys.add("numbered","SWITCH","specify the switching function to use between two sets of indistinguishable atoms");
keys.linkActionInDocs("SWITCH","LESS_THAN");
keys.addActionNameSuffix("_PROPER"); keys.needsAction("TRANSPOSE"); keys.needsAction("CONCATENATE");
}

Expand Down
1 change: 0 additions & 1 deletion src/adjmat/DistanceMatrix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */
#include "AdjacencyMatrixBase.h"
#include "core/ActionRegister.h"
#include "tools/SwitchingFunction.h"
#include "tools/Matrix.h"

//+PLUMEDOC MATRIX DISTANCE_MATRIX
Expand Down
3 changes: 3 additions & 0 deletions src/adjmat/HbondMatrix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,13 @@ void HbondMatrix::registerKeywords( Keywords& keys ) {
keys.add("atoms","HYDROGENS","The list of atoms that can form the bridge between the two interesting parts "
"of the structure.");
keys.add("numbered","SWITCH","The switchingfunction that specifies how close a pair of atoms must be together for there to be a hydrogen bond between them");
keys.linkActionInDocs("SWITCH","LESS_THAN");
keys.add("numbered","HSWITCH","The switchingfunction that specifies how close the hydrogen must be to the donor atom of the hydrogen bond for it to be "
"considered a hydrogen bond");
keys.linkActionInDocs("HSWITCH","LESS_THAN");
keys.add("numbered","ASWITCH","A switchingfunction that is used to specify what the angle between the vector connecting the donor atom to the acceptor atom and "
"the vector connecting the donor atom to the hydrogen must be in order for it considered to be a hydrogen bond");
keys.linkActionInDocs("ASWITCH","LESS_THAN");
}

HbondMatrix::HbondMatrix(const ActionOptions&ao):
Expand Down
4 changes: 4 additions & 0 deletions src/adjmat/TopologyMatrix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,14 @@ void TopologyMatrix::registerKeywords( Keywords& keys ) {
keys.add("compulsory","SWITCH","This keyword is used if you want to employ an alternative to the continuous swiching function defined above. "
"The following provides information on the \\ref switchingfunction that are available. "
"When this keyword is present you no longer need the NN, MM, D_0 and R_0 keywords.");
keys.linkActionInDocs("SWITCH","LESS_THAN");
keys.add("compulsory","RADIUS","");
keys.linkActionInDocs("RADIUS","LESS_THAN");
keys.add("compulsory","CYLINDER_SWITCH","a switching function on ( r_ij . r_ik - 1 )/r_ij");
keys.linkActionInDocs("CYLINDER_SWITCH","LESS_THAN");
keys.add("compulsory","BIN_SIZE","the size to use for the bins");
keys.add("compulsory","DENSITY_THRESHOLD","");
keys.linkActionInDocs("DENSITY_THRESHOLD","LESS_THAN");
keys.add("compulsory","SIGMA","the width of the function to be used for kernel density estimation");
keys.add("compulsory","KERNEL","gaussian","the type of kernel function to be used");
}
Expand Down
15 changes: 12 additions & 3 deletions src/cltools/GenJson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class GenJson : public CLTool {
private:
std::string version;
void printHyperlink(std::string action );
void printKeywordDocs( const std::string& k, const std::string& mydescrip, const Keywords& keys );
public:
static void registerKeywords( Keywords& keys );
explicit GenJson(const CLToolOptions& co );
Expand Down Expand Up @@ -90,6 +91,10 @@ void GenJson::printHyperlink( std::string action ) {
std::cout<<".html\","<<std::endl;
}

void GenJson::printKeywordDocs( const std::string& k, const std::string& mydescrip, const Keywords& keys ) {
std::cout<<" \""<<k<<"\" : { \"type\": \""<<keys.getStyle(k)<<"\", \"description\": \""<<mydescrip<<"\", \"multiple\": "<<keys.numbered(k)<<", \"actionlink\": \""<<keys.getLinkedActions(k)<<"\"";
}

int GenJson::main(FILE* in, FILE*out,Communicator& pc) {
std::string line(""), actionfile; parse("--actions",actionfile);
IFile myfile; myfile.open(actionfile); bool stat;
Expand Down Expand Up @@ -133,9 +138,13 @@ int GenJson::main(FILE* in, FILE*out,Communicator& pc) {
std::size_t dot=desc.find_first_of("."); std::string mydescrip = desc.substr(0,dot);
if( mydescrip.find("\\")!=std::string::npos ) error("found invalid backslash character documentation for keyword " + keys.getKeyword(j) + " in action " + action_names[i] );
std::string argtype = keys.getArgumentType( keys.getKeyword(j) );
if( argtype.length()>0 ) std::cout<<" \""<<keys.getKeyword(j)<<"\" : { \"type\": \""<<keys.getStyle(keys.getKeyword(j))<<"\", \"description\": \""<<mydescrip<<"\", \"multiple\": "<<keys.numbered( keys.getKeyword(j) )<<", \"argtype\": \""<<argtype<<"\"}";
else if( defa.length()>0 ) std::cout<<" \""<<keys.getKeyword(j)<<"\" : { \"type\": \""<<keys.getStyle(keys.getKeyword(j))<<"\", \"description\": \""<<mydescrip<<"\", \"multiple\": "<<keys.numbered( keys.getKeyword(j) )<<", \"default\": \""<<defa<<"\"}";
else std::cout<<" \""<<keys.getKeyword(j)<<"\" : { \"type\": \""<<keys.getStyle(keys.getKeyword(j))<<"\", \"description\": \""<<mydescrip<<"\", \"multiple\": "<<keys.numbered( keys.getKeyword(j) )<<"}";
if( argtype.length()>0 ) {
printKeywordDocs( keys.getKeyword(j), mydescrip, keys ); std::cout<<", \"argtype\": \""<<argtype<<"\"}";
} else if( defa.length()>0 ) {
printKeywordDocs( keys.getKeyword(j), mydescrip, keys ); std::cout<<", \"default\": \""<<defa<<"\"}";
} else {
printKeywordDocs( keys.getKeyword(j), mydescrip, keys ); std::cout<<"}";
}
if( j==keys.size()-1 && !keys.exists("HAS_VALUES") ) std::cout<<std::endl; else std::cout<<","<<std::endl;
}
if( keys.exists("HAS_VALUES") ) {
Expand Down
2 changes: 1 addition & 1 deletion src/colvar/ContactMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ void ContactMap::registerKeywords( Keywords& keys ) {
keys.add("numbered","WEIGHT","A weight value for a given contact, by default is 1.0 "
"You can either specify a global weight value using WEIGHT or one "
"weight value for each contact.");
keys.reset_style("SWITCH","compulsory");
keys.reset_style("SWITCH","compulsory"); keys.linkActionInDocs("SWITCH","LESS_THAN");
keys.addFlag("SUM",false,"calculate the sum of all the contacts in the input");
keys.addFlag("CMDIST",false,"calculate the distance with respect to the provided reference contact map");
keys.addFlag("SERIAL",false,"Perform the calculation in serial - for debug purpose");
Expand Down
1 change: 1 addition & 0 deletions src/colvar/Coordination.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ void Coordination::registerKeywords( Keywords& keys ) {
keys.add("optional","SWITCH","This keyword is used if you want to employ an alternative to the continuous switching function defined above. "
"The following provides information on the \\ref switchingfunction that are available. "
"When this keyword is present you no longer need the NN, MM, D_0 and R_0 keywords.");
keys.linkActionInDocs("SWITCH","LESS_THAN");
keys.setValueDescription("scalar","the value of the coordination");
}

Expand Down
1 change: 0 additions & 1 deletion src/function/Bessel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
#include "FunctionOfScalar.h"
#include "FunctionOfVector.h"
#include "core/ActionRegister.h"
#include "tools/SwitchingFunction.h"
#include <array>
#include <cmath>

Expand Down
51 changes: 51 additions & 0 deletions src/function/Between.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,57 @@ namespace function {
/*
Use a switching function to determine how many of the input variables are within a certain range.

If we have multiple instances of a variable we can estimate the probability density function
for that variable using a process called kernel density estimation:

\f[
P(s) = \sum_i K\left( \frac{s - s_i}{w} \right)
\f]

In this equation \f$K\f$ is a symmetric function that must integrate to one that is often
called a kernel function and \f$w\f$ is a smearing parameter. From a probability density function calculated using
kernel density estimation we can calculate the number/fraction of values between an upper and lower
bound using:

\f[
w(s) = \int_a^b \sum_i K\left( \frac{s - s_i}{w} \right)
\f]

All the input to calculate a quantity like \f$w(s)\f$ is generally provided through a single
keyword that will have the following form:

KEYWORD={TYPE UPPER=\f$a\f$ LOWER=\f$b\f$ SMEAR=\f$\frac{w}{b-a}\f$}

This will calculate the number of values between \f$a\f$ and \f$b\f$. To calculate
the fraction of values you add the word NORM to the input specification. If the
function keyword SMEAR is not present \f$w\f$ is set equal to \f$0.5(b-a)\f$. Finally,
type should specify one of the kernel types that is present in plumed. These are listed
in the table below:

<table align=center frame=void width=95%% cellpadding=5%%>
<tr>
<td> TYPE </td> <td> FUNCTION </td>
</tr> <tr>
<td> GAUSSIAN </td> <td> \f$\frac{1}{\sqrt{2\pi}w} \exp\left( -\frac{(s-s_i)^2}{2w^2} \right)\f$ </td>
</tr> <tr>
<td> TRIANGULAR </td> <td> \f$ \frac{1}{2w} \left( 1. - \left| \frac{s-s_i}{w} \right| \right) \quad \frac{s-s_i}{w}<1 \f$ </td>
</tr>
</table>

Some keywords can also be used to calculate a discrete version of the histogram. That
is to say the number of values between \f$a\f$ and \f$b\f$, the number of values between
\f$b\f$ and \f$c\f$ and so on. A keyword that specifies this sort of calculation would look
something like

KEYWORD={TYPE UPPER=\f$a\f$ LOWER=\f$b\f$ NBINS=\f$n\f$ SMEAR=\f$\frac{w}{n(b-a)}\f$}

This specification would calculate the following vector of quantities:

\f[
w_j(s) = \int_{a + \frac{j-1}{n}(b-a)}^{a + \frac{j}{n}(b-a)} \sum_i K\left( \frac{s - s_i}{w} \right)
\f]


\par Examples

*/
Expand Down
137 changes: 137 additions & 0 deletions src/function/LessThan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,143 @@ namespace function {
/*
Use a switching function to determine how many of the input variables are less than a certain cutoff.

Switching functions \f$s(r)\f$ take a minimum of one input parameter \f$r_0\f$.
For \f$r \le d_0 \quad s(r)=1.0\f$ while for \f$r > d_0\f$ the function decays smoothly to 0.
The various switching functions available in PLUMED differ in terms of how this decay is performed.

Where there is an accepted convention in the literature (e.g. \ref COORDINATION) on the form of the
switching function we use the convention as the default. However, the flexibility to use different
switching functions is always present generally through a single keyword. This keyword generally
takes an input with the following form:

\verbatim
KEYWORD={TYPE <list of parameters>}
\endverbatim

The following table contains a list of the various switching functions that are available in PLUMED 2
together with an example input.

<table align=center frame=void width=95%% cellpadding=5%%>
<tr>
<td> TYPE </td> <td> FUNCTION </td> <td> EXAMPLE INPUT </td> <td> DEFAULT PARAMETERS </td>
</tr> <tr> <td>RATIONAL </td> <td>
\f$
s(r)=\frac{ 1 - \left(\frac{ r - d_0 }{ r_0 }\right)^{n} }{ 1 - \left(\frac{ r - d_0 }{ r_0 }\right)^{m} }
\f$
</td> <td>
{RATIONAL R_0=\f$r_0\f$ D_0=\f$d_0\f$ NN=\f$n\f$ MM=\f$m\f$}
</td> <td> \f$d_0=0.0\f$, \f$n=6\f$, \f$m=2n\f$ </td>
</tr> <tr>
<td> EXP </td> <td>
\f$
s(r)=\exp\left(-\frac{ r - d_0 }{ r_0 }\right)
\f$
</td> <td>
{EXP R_0=\f$r_0\f$ D_0=\f$d_0\f$}
</td> <td> \f$d_0=0.0\f$ </td>
</tr> <tr>
<td> GAUSSIAN </td> <td>
\f$
s(r)=\exp\left(-\frac{ (r - d_0)^2 }{ 2r_0^2 }\right)
\f$
</td> <td>
{GAUSSIAN R_0=\f$r_0\f$ D_0=\f$d_0\f$}
</td> <td> \f$d_0=0.0\f$ </td>
</tr> <tr>
<td> SMAP </td> <td>
\f$
s(r) = \left[ 1 + ( 2^{a/b} -1 )\left( \frac{r-d_0}{r_0} \right)^a \right]^{-b/a}
\f$
</td> <td>
{SMAP R_0=\f$r_0\f$ D_0=\f$d_0\f$ A=\f$a\f$ B=\f$b\f$}
</td> <td> \f$d_0=0.0\f$ </td>
</tr> <tr>
<td> Q </td> <td>
\f$
s(r) = \frac{1}{1 + \exp(\beta(r_{ij} - \lambda r_{ij}^0))}
\f$
</td> <td>
{Q REF=\f$r_{ij}^0\f$ BETA=\f$\beta\f$ LAMBDA=\f$\lambda\f$ }
</td> <td> \f$\lambda=1.8\f$, \f$\beta=50 nm^-1\f$ (all-atom)<br/>\f$\lambda=1.5\f$, \f$\beta=50 nm^-1\f$ (coarse-grained) </td>
</tr> <tr>
<td> CUBIC </td> <td>
\f$
s(r) = (y-1)^2(1+2y) \qquad \textrm{where} \quad y = \frac{r - r_1}{r_0-r_1}
\f$
</td> <td>
{CUBIC D_0=\f$r_1\f$ D_MAX=\f$r_0\f$}
</td> <td> </td>
</tr> <tr>
<td> TANH </td> <td>
\f$
s(r) = 1 - \tanh\left( \frac{ r - d_0 }{ r_0 } \right)
\f$
</td> <td>
{TANH R_0=\f$r_0\f$ D_0=\f$d_0\f$}
</td> <td> </td>
</tr> <tr>
<td> COSINUS </td> <td>
\f$s(r) =\left\{\begin{array}{ll}
1 & \mathrm{if } r \leq d_0 \\
0.5 \left( \cos ( \frac{ r - d_0 }{ r_0 } \pi ) + 1 \right) & \mathrm{if } d_0 < r\leq d_0 + r_0 \\
0 & \mathrm{if } r > d_0 + r_0
\end{array}\right.
\f$
</td> <td>
{COSINUS R_0=\f$r_0\f$ D_0=\f$d_0\f$}
</td> <td> </td>
</tr> <tr>
<td> CUSTOM </td> <td>
\f$
s(r) = FUNC
\f$
</td> <td>
{CUSTOM FUNC=1/(1+x^6) R_0=\f$r_0\f$ D_0=\f$d_0\f$}
</td> <td> </td>
</tr>
</table>

Notice that most commonly used rational functions are better optimized and might run faster.

Notice that for backward compatibility we allow using `MATHEVAL` instead of `CUSTOM`.
Also notice that if the a `CUSTOM` switching function only depends on even powers of `x` it can be
made faster by using `x2` as a variable. For instance
\verbatim
{CUSTOM FUNC=1/(1+x2^3) R_0=0.3}
\endverbatim
is equivalent to
\verbatim
{CUSTOM FUNC=1/(1+x^6) R_0=0.3}
\endverbatim
but runs faster. The reason is that there is an expensive square root calculation that can be optimized out.


\attention
With the default implementation CUSTOM is slower than other functions
(e.g., it is slower than an equivalent RATIONAL function by approximately a factor 2).
Checkout page \ref Lepton to see how to improve its performance.

For all the switching functions in the above table one can also specify a further (optional) parameter using the parameter
keyword D_MAX to assert that for \f$r>d_{\textrm{max}}\f$ the switching function can be assumed equal to zero.
In this case the function is brought smoothly to zero by stretching and shifting it.
\verbatim
KEYWORD={RATIONAL R_0=1 D_MAX=3}
\endverbatim
the resulting switching function will be
\f$
s(r) = \frac{s'(r)-s'(d_{max})}{s'(0)-s'(d_{max})}
\f$
where
\f$
s'(r)=\frac{1-r^6}{1-r^{12}}
\f$
Since PLUMED 2.2 this is the default. The old behavior (no stretching) can be obtained with the
NOSTRETCH flag. The NOSTRETCH keyword is only provided for backward compatibility and might be
removed in the future. Similarly, the STRETCH keyword is still allowed but has no effect.

Notice that switching functions defined with the simplified syntax are never stretched
for backward compatibility. This might change in the future.

\par Examples

*/
Expand Down
3 changes: 3 additions & 0 deletions src/multicolvar/MultiColvarShortcuts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ void MultiColvarShortcuts::shortcutKeywords( Keywords& keys ) {
keys.add("numbered","LESS_THAN","calculate the number of variables that are less than a certain target value. "
"This quantity is calculated using \\f$\\sum_i \\sigma(s_i)\\f$, where \\f$\\sigma(s)\\f$ "
"is a \\ref switchingfunction.");
keys.linkActionInDocs("LESS_THAN","LESS_THAN");
keys.addOutputComponent("lessthan","LESS_THAN","scalar","the number of colvars that have a value less than a threshold");
keys.add("numbered","MORE_THAN","calculate the number of variables that are more than a certain target value. "
"This quantity is calculated using \\f$\\sum_i 1 - \\sigma(s_i)\\f$, where \\f$\\sigma(s)\\f$ "
"is a \\ref switchingfunction.");
keys.linkActionInDocs("MORE_THAN","MORE_THAN");
keys.addOutputComponent("morethan","MORE_THAN","scalar","the number of colvars that have a value more than a threshold");
keys.add("optional","ALT_MIN","calculate the minimum value. "
"To make this quantity continuous the minimum is calculated using "
Expand All @@ -54,6 +56,7 @@ void MultiColvarShortcuts::shortcutKeywords( Keywords& keys ) {
keys.add("numbered","BETWEEN","calculate the number of values that are within a certain range. "
"These quantities are calculated using kernel density estimation as described on "
"\\ref histogrambead.");
keys.linkActionInDocs("BETWEEN","BETWEEN");
keys.addOutputComponent("between","BETWEEN","scalar","the number of colvars that have a value that lies in a particular interval");
keys.addFlag("HIGHEST",false,"this flag allows you to recover the highest of these variables.");
keys.addOutputComponent("highest","HIGHEST","scalar","the largest of the colvars");
Expand Down
1 change: 1 addition & 0 deletions src/secondarystructure/SecondaryStructureRMSD.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ void SecondaryStructureRMSD::registerKeywords( Keywords& keys ) {
keys.addFlag("VERBOSE",false,"write a more detailed output");
keys.add("optional","LESS_THAN","calculate the number of a residue segments that are within a certain target distance of this secondary structure type. "
"This quantity is calculated using \\f$\\sum_i \\sigma(s_i)\\f$, where \\f$\\sigma(s)\\f$ is a \\ref switchingfunction.");
keys.linkActionInDocs("LESS_THAN","LESS_THAN");
keys.add("optional","R_0","The r_0 parameter of the switching function.");
keys.add("compulsory","D_0","0.0","The d_0 parameter of the switching function");
keys.add("compulsory","NN","8","The n parameter of the switching function");
Expand Down
1 change: 1 addition & 0 deletions src/symfunc/CoordinationNumbers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ void CoordinationNumbers::shortcutKeywords( Keywords& keys ) {
keys.add("compulsory","D_0","0.0","The d_0 parameter of the switching function");
keys.add("compulsory","R_0","The r_0 parameter of the switching function");
keys.add("optional","SWITCH","the switching function that it used in the construction of the contact matrix");
keys.linkActionInDocs("SWITCH","LESS_THAN");
multicolvar::MultiColvarShortcuts::shortcutKeywords( keys );
keys.needsAction("CONTACT_MATRIX"); keys.needsAction("GROUP");
}
Expand Down
2 changes: 2 additions & 0 deletions src/symfunc/SMAC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ void SMAC::registerKeywords(Keywords& keys) {
keys.add("optional","SWITCH","This keyword is used if you want to employ an alternative to the continuous swiching function defined above. "
"The following provides information on the \\ref switchingfunction that are available. "
"When this keyword is present you no longer need the NN, MM, D_0 and R_0 keywords.");
keys.linkActionInDocs("SWITCH","LESS_THAN");
keys.add("numbered","KERNEL","The kernels used in the function of the angle");
keys.add("optional","SWITCH_COORD","This keyword is used to define the coordination switching function.");
keys.linkActionInDocs("SWITCH_COORD","LESS_THAN");
keys.reset_style("KERNEL","optional");
keys.setValueDescription("vector","the value of the smac parameter for each of the input molecules");
multicolvar::MultiColvarShortcuts::shortcutKeywords( keys );
Expand Down
Loading
Loading