/* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 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