/* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Copyright (c) 2016-2018 The VES code team
(see the PEOPLE-VES file at the root of this folder for a list of names)
See http://www.ves-code.org for more information.
This file is part of VES code module.
The VES code module is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
The VES code module is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with the VES code module. If not, see .
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */
#include "bias/Bias.h"
#include "core/PlumedMain.h"
#include "core/ActionRegister.h"
#include "core/Atoms.h"
#include "tools/Communicator.h"
#include "tools/Grid.h"
#include "tools/File.h"
//#include //std::fill
namespace PLMD {
namespace ves {
//+PLUMEDOC VES_BIAS VES_DELTA_F
/*
Implementation of VES\f$\Delta F\f$ method \cite Invernizzi2019vesdeltaf (step two only).
\warning
Notice that this is a stand-alone bias Action, it does not need any of the other VES module components
First you should create some estimate of the local free energy basins of your system,
using e.g. multiple \ref METAD short runs, and combining them with the \ref sum_hills utility.
Once you have them, you can use this bias Action to perform the VES optimization part of the method.
These \f$N+1\f$ local basins are used to model the global free energy.
In particular, given the conditional probabilities \f$P(\mathbf{s}|i)\propto e^{-\beta F_i(\mathbf{s})}\f$
and the probabilities of being in a given basin \f$P_i\f$, we can write:
\f[
e^{-\beta F(\mathbf{s})}\propto P(\mathbf{s})=\sum_{i=0}^N P(\mathbf{s}|i)P_i \, .
\f]
We use this free energy model and the chosen bias factor \f$\gamma\f$ to build the bias potential:
\f$V(\mathbf{s})=-(1-1/\gamma)F(\mathbf{s})\f$.
Or, more explicitly:
\f[
V(\mathbf{s})=(1-1/\gamma)\frac{1}{\beta}\log\left[e^{-\beta F_0(\mathbf{s})}
+\sum_{i=1}^{N} e^{-\beta F_i(\mathbf{s})} e^{-\beta \alpha_i}\right] \, ,
\f]
where the parameters \f$\boldsymbol{\alpha}\f$ are the \f$N\f$ free energy differences (see below) from the \f$F_0\f$ basin.
By default the \f$F_i(\mathbf{s})\f$ are shifted so that \f$\min[F_i(\mathbf{s})]=0\f$ for all \f$i=\{0,...,N\}\f$.
In this case the optimization parameters \f$\alpha_i\f$ are the difference in height between the minima of the basins.
Using the keyword `NORMALIZE`, you can also decide to normalize the local free energies so that
\f$\int d\mathbf{s}\, e^{-\beta F_i(\mathbf{s})}=1\f$.
In this case the parameters will represent not the difference in height (which depends on the chosen CVs),
but the actual free energy difference, \f$\alpha_i=\Delta F_i\f$.
However, as discussed in Ref. \cite Invernizzi2019vesdeltaf, a better estimate of \f$\Delta F_i\f$ should be obtained through the reweighting procedure.
\par Examples
The following performs the optimization of the free energy difference between two metastable basins:
\plumedfile
cv: DISTANCE ATOMS=1,2
ves: VES_DELTA_F ...
ARG=cv
TEMP=300
FILE_F0=fesA.data
FILE_F1=fesB.data
BIASFACTOR=10.0
M_STEP=0.1
AV_STRIDE=500
PRINT_STRIDE=100
...
PRINT FMT=%g STRIDE=500 FILE=Colvar.data ARG=cv,ves.bias,ves.rct
\endplumedfile
The local FES files can be obtained as described in Sec. 4.2 of Ref. \cite Invernizzi2019vesdeltaf, i.e. for example:
- run 4 independent metad runs, all starting from basin A, and kill them as soon as they make the first transition (see e.g. \ref COMMITTOR)
- \verbatim cat HILLS* > all_HILLS \endverbatim
- \verbatim plumed sum_hills --hills all_HILLS --outfile all_fesA.dat --mintozero --min 0 --max 1 --bin 100 \endverbatim
- \verbatim awk -v n_rep=4 '{if($1!="#!" && $1!="") {for(i=1+(NF-1)/2; i<=NF; i++) $i/=n_rep;} print $0}' all_fesA.dat > fesA.data \endverbatim
The header of both FES files must be identical, and should be similar to the following:
\auxfile{fesA.data}
#! FIELDS cv file.free der_cv
#! SET min_cv 0
#! SET max_cv 1
#! SET nbins_cv 100
#! SET periodic_cv false
0 1000 0
[ ... ]
\endauxfile
\auxfile{fesB.data}
#! FIELDS cv file.free der_cv
#! SET min_cv 0
#! SET max_cv 1
#! SET nbins_cv 100
#! SET periodic_cv false
0 1000 0
[ ... ]
\endauxfile
*/
//+ENDPLUMEDOC
class VesDeltaF : public bias::Bias {
private:
double beta_;
unsigned NumParallel_;
unsigned rank_;
unsigned NumWalkers_;
bool isFirstStep_;
bool afterCalculate_;
//prob
double tot_prob_;
std::vector prob_;
std::vector< std::vector > der_prob_;
//local basins
std::vector< std::unique_ptr > grid_p_; //pointers because of GridBase::create
std::vector norm_;
//optimizer-related stuff
long unsigned mean_counter_;
unsigned mean_weight_tau_;
unsigned alpha_size_;
unsigned sym_alpha_size_;
std::vector mean_alpha_;
std::vector inst_alpha_;
std::vector past_increment2_;
double minimization_step_;
bool damping_off_;
//'tg' -> 'target distribution'
double inv_gamma_;
unsigned tg_counter_;
unsigned tg_stride_;
std::vector tg_dV_dAlpha_;
std::vector tg_d2V_dAlpha2_;
//'av' -> 'ensemble average'
unsigned av_counter_;
unsigned av_stride_;
std::vector av_dV_dAlpha_;
std::vector av_dV_dAlpha_prod_;
std::vector av_d2V_dAlpha2_;
//printing
unsigned print_stride_;
OFile alphaOfile_;
//other
std::vector exp_alpha_;
std::vector prev_exp_alpha_;
double work_;
//functions
void update_alpha();
void update_tg_and_rct();
inline unsigned get_index(const unsigned, const unsigned) const;
public:
explicit VesDeltaF(const ActionOptions&);
void calculate() override;
void update() override;
static void registerKeywords(Keywords& keys);
};
PLUMED_REGISTER_ACTION(VesDeltaF,"VES_DELTA_F")
void VesDeltaF::registerKeywords(Keywords& keys) {
Bias::registerKeywords(keys);
keys.use("ARG");
keys.add("optional","TEMP","temperature is compulsory, but it can be sometimes fetched from the MD engine");
//local free energies
keys.add("numbered","FILE_F","names of files containing local free energies and derivatives. "
"The first one, FILE_F0, is used as reference for all the free energy differences.");
keys.reset_style("FILE_F","compulsory");
keys.addFlag("NORMALIZE",false,"normalize all local free energies so that alpha will be (approx) \\f$\\Delta F\\f$");
keys.addFlag("NO_MINTOZERO",false,"leave local free energies as provided, without shifting them to zero min");
//target distribution
keys.add("compulsory","BIASFACTOR","0","the \\f$\\gamma\\f$ bias factor used for well-tempered target \\f$p(\\mathbf{s})\\f$."
" Set to 0 for non-tempered flat target");
keys.add("optional","TG_STRIDE","( default=1 ) number of AV_STRIDE between updates"
" of target \\f$p(\\mathbf{s})\\f$ and reweighing factor \\f$c(t)\\f$");
//optimization
keys.add("compulsory","M_STEP","1.0","the \\f$\\mu\\f$ step used for the \\f$\\Omega\\f$ functional minimization");
keys.add("compulsory","AV_STRIDE","500","number of simulation steps between alpha updates");
keys.add("optional","TAU_MEAN","exponentially decaying average for alpha (rescaled using AV_STRIDE)."
" Should be used only in very specific cases");
keys.add("optional","INITIAL_ALPHA","( default=0 ) an initial guess for the bias potential parameter alpha");
keys.addFlag("DAMPING_OFF",false,"do not use an AdaGrad-like term to rescale M_STEP");
//output parameters file
keys.add("compulsory","ALPHA_FILE","ALPHA","file name for output minimization parameters");
keys.add("optional","PRINT_STRIDE","( default=10 ) stride for printing to ALPHA_FILE");
keys.add("optional","FMT","specify format for ALPHA_FILE");
//debug flags
keys.addFlag("SERIAL",false,"perform the calculation in serial even if multiple tasks are available");
keys.addFlag("MULTIPLE_WALKERS",false,"use multiple walkers connected via MPI for the optimization");
keys.use("RESTART");
//output components
componentsAreNotOptional(keys);
keys.addOutputComponent("rct","default","the reweighting factor \\f$c(t)\\f$");
keys.addOutputComponent("work","default","the work done by the bias in one AV_STRIDE");
}
VesDeltaF::VesDeltaF(const ActionOptions&ao)
: PLUMED_BIAS_INIT(ao)
, isFirstStep_(true)
, afterCalculate_(false)
, mean_counter_(0)
, av_counter_(0)
, work_(0)
{
//set beta
const double Kb=plumed.getAtoms().getKBoltzmann();
double temp=0;
parse("TEMP",temp);
double KbT=Kb*temp;
if(KbT==0)
{
KbT=plumed.getAtoms().getKbT();
plumed_massert(KbT>0,"your MD engine does not pass the temperature to plumed, you must specify it using TEMP");
}
beta_=1.0/KbT;
//initialize probability grids using local free energies
bool spline=true;
bool sparsegrid=false;
std::string funcl="file.free"; //typical name given by sum_hills
std::vector fes_names;
for(unsigned n=0;; n++)//NB: here we start from FILE_F0 not from FILE_F1
{
std::string filename;
if(!parseNumbered("FILE_F",n,filename))
break;
fes_names.push_back(filename);
IFile gridfile;
gridfile.open(filename);
auto g=GridBase::create(funcl,getArguments(),gridfile,sparsegrid,spline,true);
// we assume this cannot be sparse. in case we want it to be sparse, some of the methods
// that are available only in Grid should be ported to GridBase
auto gg=dynamic_cast(g.get());
// if this throws, g is deleted
plumed_assert(gg);
// release ownership in order to transfer it to emplaced pointer
g.release();
grid_p_.emplace_back(gg);
}
plumed_massert(grid_p_.size()>1,"at least 2 basins must be defined, starting from FILE_F0");
alpha_size_=grid_p_.size()-1;
sym_alpha_size_=alpha_size_*(alpha_size_+1)/2; //useful for symmetric matrix [alpha_size_]x[alpha_size_]
//check for consistency with first local free energy
for(unsigned n=1; ngetSize()==grid_p_[0]->getSize(),error_tag);
plumed_massert(grid_p_[n]->getMin()==grid_p_[0]->getMin(),error_tag);
plumed_massert(grid_p_[n]->getMax()==grid_p_[0]->getMax(),error_tag);
plumed_massert(grid_p_[n]->getBinVolume()==grid_p_[0]->getBinVolume(),error_tag);
}
bool no_mintozero=false;
parseFlag("NO_MINTOZERO",no_mintozero);
if(!no_mintozero)
{
for(unsigned n=0; nsetMinToZero();
}
bool normalize=false;
parseFlag("NORMALIZE",normalize);
norm_.resize(grid_p_.size(),0);
std::vector c_norm(grid_p_.size());
//convert the FESs to probability distributions
//NB: the spline interpolation will be done on the probability distributions, not on the given FESs
const unsigned ncv=getNumberOfArguments(); //just for ease
for(unsigned n=0; ngetSize(); t++)
{
std::vector der(ncv);
const double val=std::exp(-beta_*grid_p_[n]->getValueAndDerivatives(t,der));
for(unsigned s=0; ssetValueAndDerivatives(t,val,der);
norm_[n]+=val;
}
c_norm[n]=1./beta_*std::log(norm_[n]);
if(normalize)
{
grid_p_[n]->scaleAllValuesAndDerivatives(1./norm_[n]);
norm_[n]=1;
}
}
//get target
double biasfactor=0;
parse("BIASFACTOR",biasfactor);
plumed_massert(biasfactor==0 || biasfactor>1,"BIASFACTOR must be zero (for uniform target) or greater than one");
if(biasfactor==0)
inv_gamma_=0;
else
inv_gamma_=1./biasfactor;
tg_counter_=0;
tg_stride_=1;
parse("TG_STRIDE",tg_stride_);
tg_dV_dAlpha_.resize(alpha_size_,0);
tg_d2V_dAlpha2_.resize(sym_alpha_size_,0);
//setup optimization stuff
minimization_step_=1;
parse("M_STEP",minimization_step_);
av_stride_=500;
parse("AV_STRIDE",av_stride_);
av_dV_dAlpha_.resize(alpha_size_,0);
av_dV_dAlpha_prod_.resize(sym_alpha_size_,0);
av_d2V_dAlpha2_.resize(sym_alpha_size_,0);
mean_weight_tau_=0;
parse("TAU_MEAN",mean_weight_tau_);
if(mean_weight_tau_!=1) //set it to 1 for basic SGD
{
plumed_massert((mean_weight_tau_==0 || mean_weight_tau_>av_stride_),"TAU_MEAN is rescaled with AV_STRIDE, so it has to be greater");
mean_weight_tau_/=av_stride_; //this way you can look at the number of simulation steps to choose TAU_MEAN
}
parseVector("INITIAL_ALPHA",mean_alpha_);
if(mean_alpha_.size()>0)
{
plumed_massert(mean_alpha_.size()==alpha_size_,"provide one INITIAL_ALPHA for each basin beyond the first one");
}
else
mean_alpha_.resize(alpha_size_,0);
inst_alpha_=mean_alpha_;
exp_alpha_.resize(alpha_size_);
for(unsigned i=0; i1) //if each walker has more than one processor update them all
comm.Bcast(NumWalkers_,0);
}
checkRead();
//restart if needed
if(getRestart())
{
IFile ifile;
ifile.link(*this);
if(NumWalkers_>1)
ifile.enforceSuffix("");
if(ifile.FileExist(alphaFileName))
{
log.printf(" Restarting from: %s\n",alphaFileName.c_str());
log.printf(" all options (also PRINT_STRIDE) must be consistent!\n");
log.printf(" any INITIAL_ALPHA will be overwritten\n");
ifile.open(alphaFileName);
double time;
std::vector damping(alpha_size_);
while(ifile.scanField("time",time)) //room for improvements: only last line is important
{
for(unsigned i=0; i1)
{
if(comm.Get_rank()==0 && multi_sim_comm.Get_rank()>0)
alphaFileName="/dev/null"; //only first walker writes on file
alphaOfile_.enforceSuffix("");
}
alphaOfile_.open(alphaFileName);
if(fmt.length()>0)
alphaOfile_.fmtField(" "+fmt);
//add other output components
addComponent("rct"); componentIsNotPeriodic("rct");
addComponent("work"); componentIsNotPeriodic("work");
//print some info
log.printf(" Temperature T: %g\n",1./(Kb*beta_));
log.printf(" Beta (1/Kb*T): %g\n",beta_);
log.printf(" Local free energy basins files and normalization constants:\n");
for(unsigned n=0; n1)
log.printf(" Exponentially decaying average with weight=tau/av_stride=%d\n",mean_weight_tau_);
if(mean_weight_tau_==1)
log.printf(" +++ WARNING +++ setting TAU_MEAN=1 is equivalent to use simple SGD, without mean alpha nor hessian contribution\n");
log.printf(" Initial guess for alpha:\n");
for(unsigned i=0; i1)
log.printf(" Using multiple threads per simulation: %d\n",NumParallel_);
if(multiple_walkers)
{
log.printf(" -- MULTIPLE_WALKERS: multiple simulations will combine statistics for the optimization\n");
if(NumWalkers_>1)
{
log.printf(" number of walkers: %d\n",NumWalkers_);
log.printf(" walker rank: %d\n",multi_sim_comm.Get_rank()); //only comm.Get_rank()=0 prints, so this is fine
}
else
log.printf(" +++ WARNING +++ only one replica found: are you sure you are running MPI-connected simulations?\n");
}
log.printf(" Bibliography ");
log<0)
log<(getNumberOfArguments()));
update_tg_and_rct();
}
void VesDeltaF::calculate()
{
//get CVs
const unsigned ncv=getNumberOfArguments(); //just for ease
std::vector cv(ncv);
for(unsigned s=0; sgetValueAndDerivatives(cv,der_prob_[n]);
tot_prob_=prob_[0];
for(unsigned i=0; i dV_dAlpha(alpha_size_);
std::vector d2V_dAlpha2(sym_alpha_size_);
for(unsigned i=0; igetSize(); t+=NumParallel_)
{ //TODO can we recycle some code?
std::vector prob(grid_p_.size());
for(unsigned n=0; ngetValue(t);
double tot_prob=prob[0];
for(unsigned i=0; i dV_dAlpha(alpha_size_);
std::vector d2V_dAlpha2(sym_alpha_size_);
for(unsigned i=0; i1)
{
comm.Sum(Z_tg);
comm.Sum(tg_dV_dAlpha_);
comm.Sum(tg_d2V_dAlpha2_);
}
for(unsigned i=0; iset(-1./beta_*std::log(Z_tg/Z_0)); //Z_tg is the best available estimate of Z_V
}
void VesDeltaF::update_alpha()
{
//combining the averages of multiple walkers
if(NumWalkers_>1)
{
if(comm.Get_rank()==0) //sum only once: in the first rank of each walker
{
multi_sim_comm.Sum(av_dV_dAlpha_);
multi_sim_comm.Sum(av_dV_dAlpha_prod_);
multi_sim_comm.Sum(av_d2V_dAlpha2_);
for(unsigned i=0; i1)//if there are more ranks for each walker, everybody has to know
{
comm.Bcast(av_dV_dAlpha_,0);
comm.Bcast(av_dV_dAlpha_prod_,0);
comm.Bcast(av_d2V_dAlpha2_,0);
}
}
//set work and reset it
getPntrToComponent("work")->set(work_);
work_=0;
//build the gradient and the Hessian of the functional
std::vector grad_omega(alpha_size_);
std::vector hess_omega(sym_alpha_size_);
for(unsigned i=0; i0 && mean_weight_tau_ damping(alpha_size_);
for(unsigned i=0; i