LCOV - code coverage report
Current view: top level - ttsketch - TTMetaD.cpp (source / functions) Hit Total Coverage
Test: plumed test coverage Lines: 653 693 94.2 %
Date: 2026-11-05 10:19:04 Functions: 20 21 95.2 %

          Line data    Source code
       1             : /* Copyright (c) 2026, Nils E. Strand, Siyao Yang, Yuehaw Khoo, and Aaron R. Dinner
       2             :  *
       3             :  * Tensor train (TT)-metadynamics.
       4             :  * https://arxiv.org/abs/2603.13549
       5             :  * See module.md for installation instructions.
       6             :  */
       7             : 
       8             : #include "TTHelper.h"
       9             : #include "bias/Bias.h"
      10             : #include "core/ActionRegister.h"
      11             : #include "core/ActionSet.h"
      12             : #include "core/PlumedMain.h"
      13             : #include "tools/Exception.h"
      14             : #include "tools/Communicator.h"
      15             : #include "tools/File.h"
      16             : #include "tools/OpenMP.h"
      17             : #include "tools/OFile.h"
      18             : #include "tools/IFile.h"
      19             : #include <algorithm>
      20             : #include <ctime>
      21             : #include <filesystem>
      22             : #include <random>
      23             : 
      24             : namespace PLMD {
      25             : namespace ttsketch {
      26             : 
      27             : //+PLUMEDOC BIAS TTMETAD
      28             : /*
      29             : Performs metadynamics with a tensor-train representation of the bias potential.
      30             : 
      31             : This action runs metadynamics while periodically compressing the accumulated
      32             : Gaussian hills into a low-rank tensor train (TT) using the TT-Sketch algorithm.
      33             : The TT representation keeps memory and evaluation cost bounded throughout long
      34             : simulations, in contrast to standard metadynamics where cost grows linearly with
      35             : the number of deposited hills.
      36             : 
      37             : The basis functions of each argument are defined on [INTERVAL_MIN, INTERVAL_MAX] and are
      38             : periodic over this interval. For a periodic argument the interval should thus coincide with the
      39             : periodicity domain. For a non periodic argument the interval should contain the region that is
      40             : explored: outside it the bias is the one at the closest limit and there is no biasing force along
      41             : that argument, and hills deposited outside are folded back into the interval when they are
      42             : compressed into the TT (a warning is written in the log).
      43             : 
      44             : After every compression the TT is appended to the HDF5 file given by TT_FILE, and the hills file
      45             : restarts empty. When restarting, the last TT in TT_FILE is read back together with the hills in FILE,
      46             : i.e. those deposited after it. With multiple replicas each replica has its own TT_FILE (with the
      47             : replica suffix), unless WALKERS_MPI is used, in which case the walkers share one TT_FILE next to
      48             : the shared hills file. Two TTMETAD actions must use different TT_FILE.
      49             : 
      50             : ## Examples
      51             : 
      52             : The following input runs TT-MetaD on two distances. Hills are deposited every
      53             : 500 steps and compressed into a TT every 1e6 steps with target rank 10.
      54             : 
      55             : ```plumed
      56             : d1: DISTANCE ATOMS=1,2
      57             : d2: DISTANCE ATOMS=3,4
      58             : TTMETAD ...
      59             :   ARG=d1,d2
      60             :   SIGMA=0.1,0.1
      61             :   HEIGHT=1.0
      62             :   PACE=500
      63             :   SKETCH_PACE=1000000
      64             :   SKETCH_RANK=10
      65             :   SKETCH_INITRANK=50
      66             :   INTERVAL_MIN=0.0,0.0
      67             :   INTERVAL_MAX=3.0,3.0
      68             :   FILE=HILLS
      69             :   LABEL=tt
      70             : ... TTMETAD
      71             : PRINT ARG=tt.bias FILE=COLVAR STRIDE=100
      72             : ```
      73             : 
      74             : */
      75             : //+ENDPLUMEDOC
      76             : 
      77             : using namespace PLMD::bias;
      78             : 
      79             : #if !defined(__PLUMED_HAS_LIBITENSOR) || !defined(__PLUMED_HAS_LIBHDF5)
      80             : 
      81             : class TTMetaD : public Bias {
      82             : public:
      83             :   static void registerKeywords(Keywords& keys);
      84             :   explicit TTMetaD(const ActionOptions& ao) : Action(ao), Bias(ao) {
      85             :     error("TTMETAD requires libitensor and libhdf5. "
      86             :           "Reconfigure with --enable-libitensor.");
      87             :   }
      88             :   void calculate() override {}
      89             :   void update() override {}
      90             : };
      91             : 
      92             : #else
      93             : 
      94             : using namespace itensor;
      95             : 
      96             : class TTMetaD : public Bias {
      97             : 
      98             : private:
      99             :   // singular values of the cross-gram matrices smaller than this fraction of the largest
     100             :   // one are not inverted when the TT cores are recovered (see paraSketch())
     101             :   static constexpr double svdTolerance = 1e-12;
     102             :   struct Gaussian {
     103             :     double height;
     104             :     std::vector<double> center;
     105             :     std::vector<double> sigma;
     106             :     std::vector<double> invsigma;
     107         355 :     Gaussian(const double h, const std::vector<double>& c, const std::vector<double>& s)
     108         355 :       : height(h), center(c), sigma(s), invsigma(s) {
     109        1135 :       for(unsigned i = 0; i < invsigma.size(); ++i) {
     110         780 :         if(abs(invsigma[i]) > 1.e-20) {
     111         780 :           invsigma[i] = 1.0 / invsigma[i];
     112             :         } else {
     113           0 :           invsigma[i] = 0.0;
     114             :         }
     115             :       }
     116         355 :     }
     117             :   };
     118             :   // standard metadynamics parameters
     119             :   double kbt_;
     120             :   int stride_;
     121             :   bool welltemp_;
     122             :   double biasf_;
     123             :   std::string fmt_;
     124             :   bool isFirstStep_;
     125             :   double height0_;
     126             :   std::vector<double> sigma0_;
     127             :   std::vector<Gaussian> hills_;   // Gaussians deposited since the last TT update
     128             :   OFile hillsOfile_;
     129             :   std::string mw_dir_;
     130             :   std::string hillsfname_;
     131             :   bool walkers_mpi_;
     132             :   int mpi_size_;
     133             :   int mpi_rank_;
     134             :   // TT-sketch parameters
     135             :   unsigned d_;               // number of CV dimensions
     136             :   int sketch_rc_;            // initial (sketch) rank for the random TT coefficient tensor
     137             :   int sketch_r_;             // target TT bond dimension after SVD truncation (0 = use cutoff)
     138             :   double sketch_cutoff_;     // relative SVD truncation threshold (used when sketch_r_=0)
     139             :   int sketch_stride_;        // steps between TT bias updates (tau in the paper)
     140             :   double sketch_alpha_;      // weight for non-constant basis functions in the sketch coefficient TT
     141             :   std::vector<BasisFunc> sketch_basis_;  // one BasisFunc per CV dimension
     142             :   unsigned sketch_count_;    // number of TT updates performed so far (starts at 1, increments after each update)
     143             :   MPS vb_;                   // current TT approximation of the accumulated bias (V_bias^TT)
     144             :   DenseTT vbDense_;          // plain-array copy of vb_ used to evaluate the bias (rebuilt by setVb())
     145             :   std::string ttfilename_;   // HDF5 file where the TTs are stored (TT_FILE, with replica suffix or in the walkers directory)
     146             :   std::vector<double> imin_; // INTERVAL_MIN
     147             :   std::vector<double> imax_; // INTERVAL_MAX
     148             :   double sketch_until_;      // simulation time after which the bias is frozen (no further updates)
     149             :   bool frozen_;              // true once the bias has been frozen (after sketch_until_)
     150             :   bool sketch_conv_;         // true if Gaussian smoothing (conv mode) is active for basis evaluation
     151             :   bool nonintrusive_;        // if true, uses the intrusive-free variant: accumulates Bemp directly
     152             :   // instead of re-projecting the previous vb_ through the Gram matrix
     153             :   bool deterministic_;       // if true, use a fixed RNG seed (42) in createTTCoeff() for reproducibility
     154             :   MPS B_prev_;               // accumulated tensor moment from all previous sketches (nonintrusive only)
     155             :   std::vector<ITensor> A_prev_;   // accumulated cross-gram matrices per bond (nonintrusive only)
     156             : 
     157             :   void readGaussians(IFile *ifile);
     158             :   void setVb(const MPS& vb);
     159             :   // clamp cv to [INTERVAL_MIN, INTERVAL_MAX]; out[i] is true if cv[i] was outside
     160             :   void clampToInterval(std::vector<double>& cv, std::vector<char>& out) const;
     161             :   bool writerReplica() const {
     162         123 :     return !this->walkers_mpi_ || this->mpi_rank_ == 0;
     163             :   }
     164             :   void writeGaussian(const Gaussian& hill, OFile& file);
     165             :   double getHeight(const std::vector<double>& cv);
     166             :   double getBias(const std::vector<double>& cv);
     167             :   double getBiasAndDerivatives(const std::vector<double>& cv, std::vector<double>& der);
     168             :   double evaluateGaussian(const std::vector<double>& cv, const Gaussian& hill);
     169             :   double evaluateGaussianAndDerivatives(const std::vector<double>& cv, const Gaussian& hill, std::vector<double>& der, std::vector<double>& dp);
     170             :   bool scanOneHill(IFile* ifile, std::vector<Value>& tmpvalues, std::vector<double>& center, std::vector<double>& sigma, double& height);
     171             :   void paraSketch();
     172             :   MPS createTTCoeff() const;
     173             :   std::vector<Matrix<double>> intBasisSample() const;
     174             :   std::pair<MPS, std::vector<Matrix<double>>> formTensorMoment(const std::vector<Matrix<double>>& M, const MPS& coeff) const;
     175             :   std::tuple<MPS, std::vector<ITensor>, std::vector<ITensor>> formTensorMomentVb(const MPS& coeff);
     176             : 
     177             : public:
     178             :   explicit TTMetaD(const ActionOptions&);
     179             :   void calculate() override;
     180             :   void update() override;
     181             :   static void registerKeywords(Keywords& keys);
     182             : };
     183             : 
     184          16 : TTMetaD::TTMetaD(const ActionOptions& ao):
     185             :   PLUMED_BIAS_INIT(ao),
     186          16 :   kbt_(0.0),
     187          16 :   stride_(0),
     188          16 :   welltemp_(false),
     189          16 :   biasf_(-1.0),
     190          16 :   isFirstStep_(true),
     191          16 :   height0_(std::numeric_limits<double>::max()),
     192          16 :   mw_dir_(""),
     193          16 :   walkers_mpi_(false),
     194          16 :   mpi_size_(0),
     195          16 :   mpi_rank_(0),
     196          16 :   sketch_r_(0),
     197          16 :   sketch_cutoff_(0.0),
     198          16 :   sketch_count_(1),
     199          16 :   sketch_until_(std::numeric_limits<double>::max()),
     200          16 :   frozen_(false),
     201          16 :   sketch_conv_(false),
     202          16 :   nonintrusive_(false),
     203          32 :   deterministic_(false) {
     204             :   bool kernel;
     205          32 :   parseFlag("KERNEL_BASIS", kernel);
     206          16 :   this->d_ = getNumberOfArguments();
     207          16 :   if(this->d_ < 2) {
     208           0 :     error("Number of arguments must be at least 2");
     209             :   }
     210          16 :   parse("FMT", this->fmt_);
     211          32 :   parseVector("SIGMA", this->sigma0_);
     212          16 :   if(this->sigma0_.size() != d_) {
     213           0 :     error("number of arguments does not match number of SIGMA parameters");
     214             :   }
     215          16 :   parse("HEIGHT", this->height0_);
     216          16 :   parse("PACE", this->stride_);
     217          16 :   if(stride_ <= 0) {
     218           0 :     error("frequency for hill addition is nonsensical");
     219             :   }
     220          16 :   this->hillsfname_ = "HILLS";
     221          16 :   parse("FILE", this->hillsfname_);
     222          16 :   parse("BIASFACTOR", this->biasf_);
     223          16 :   if(this->biasf_ < 1.0 && this->biasf_ != -1.0) {
     224           0 :     error("well tempered bias factor is nonsensical");
     225             :   }
     226          16 :   this->kbt_ = getkBT();
     227          16 :   if(this->biasf_ >= 1.0) {
     228           2 :     if(this->kbt_ == 0.0) {
     229           0 :       error("Unless the MD engine passes the temperature to plumed, with well-tempered metad you must specify it using TEMP");
     230             :     }
     231           2 :     this->welltemp_ = true;
     232             :   }
     233             : 
     234          16 :   parseFlag("WALKERS_MPI", this->walkers_mpi_);
     235          16 :   parse("WALKERS_DIR", this->mw_dir_);
     236          16 :   if(this->walkers_mpi_ && this->mw_dir_== "") {
     237           4 :     const std::string ret = std::filesystem::current_path();
     238           4 :     this->mw_dir_ = ret + "/";
     239             :     // multi_sim_comm is valid only on the first rank of each replica
     240           4 :     if(comm.Get_rank() == 0) {
     241           2 :       multi_sim_comm.Bcast(this->mw_dir_, 0);
     242             :     }
     243           4 :     comm.Bcast(this->mw_dir_, 0);
     244             :   }
     245          16 :   if(this->walkers_mpi_ && this->mw_dir_ != "") {
     246           8 :     this->hillsfname_ = this->mw_dir_ + "/" + this->hillsfname_;
     247             :   }
     248          16 :   std::string ttfile = "ttsketch.h5";
     249          16 :   parse("TT_FILE", ttfile);
     250          16 :   if(this->walkers_mpi_) {
     251             :     // shared by all the walkers, next to the shared hills file
     252           8 :     this->ttfilename_ = this->mw_dir_ + "/" + ttfile;
     253             :   } else {
     254             :     // one file per replica, as for the other output files
     255          24 :     this->ttfilename_ = FileBase::appendSuffix(ttfile, plumed.getSuffix());
     256             :   }
     257          19 :   for(const auto* other : plumed.getActionSet().select<const TTMetaD*>()) {
     258           3 :     if(other->ttfilename_ == this->ttfilename_) {
     259           0 :       error("TT_FILE " + this->ttfilename_ + " is already used by action " + other->getLabel() + ": use a different TT_FILE");
     260             :     }
     261             :   }
     262             : 
     263          16 :   parse("SKETCH_RANK", this->sketch_r_);
     264          16 :   parse("SKETCH_CUTOFF", this->sketch_cutoff_);
     265          16 :   if(this->sketch_r_ <= 0 && (this->sketch_cutoff_ <= 0.0 || this->sketch_cutoff_ > 1.0)) {
     266           0 :     error("Valid SKETCH_RANK or SKETCH_CUTOFF needs to be specified");
     267             :   }
     268          16 :   parse("SKETCH_INITRANK", this->sketch_rc_);
     269          16 :   if(this->sketch_rc_ <= 0) {
     270           0 :     error("SKETCH_INITRANK must be positive");
     271             :   }
     272          16 :   parse("SKETCH_PACE", this->sketch_stride_);
     273          16 :   if(this->sketch_stride_ <= 0) {
     274           0 :     error("SKETCH_PACE must be positive");
     275             :   }
     276          16 :   std::vector<double> interval_min;
     277          32 :   parseVector("INTERVAL_MIN", interval_min);
     278          16 :   if(interval_min.size() != this->d_) {
     279           0 :     error("Number of arguments does not match number of INTERVAL_MIN parameters");
     280             :   }
     281          16 :   std::vector<double> interval_max;
     282          32 :   parseVector("INTERVAL_MAX", interval_max);
     283          16 :   if(interval_max.size() != this->d_) {
     284           0 :     error("Number of arguments does not match number of INTERVAL_MAX parameters");
     285             :   }
     286          16 :   int nbasis = 20;
     287          16 :   parse("SKETCH_NBASIS", nbasis);
     288          16 :   if(nbasis <= 1) {
     289           0 :     error("SKETCH_NBASIS must be greater than 1");
     290             :   }
     291          16 :   if(!kernel && nbasis % 2 == 0) {
     292           0 :     ++nbasis;
     293             :   }
     294          16 :   parse("SKETCH_ALPHA", this->sketch_alpha_);
     295          16 :   if(this->sketch_alpha_ <= 0.0 || this->sketch_alpha_ > 1.0) {
     296           0 :     error("SKETCH_ALPHA must be positive and no greater than 1");
     297             :   }
     298          16 :   std::vector<double> w;
     299          32 :   parseVector("SKETCH_WIDTH", w);
     300          16 :   if(w.size() == 0) {
     301          14 :     w.assign(this->d_, 0.0);
     302             :   }
     303          16 :   if(w.size() != this->d_) {
     304           0 :     error("Number of arguments does not match number of SKETCH_WIDTH parameters");
     305             :   }
     306          52 :   for (double val : w) {
     307          36 :     if (val != 0.0) {
     308           6 :       this->sketch_conv_ = true;
     309             :     }
     310             :   }
     311          16 :   std::vector<double> dx;
     312          32 :   parseVector("KERNEL_DX", dx);
     313          16 :   if(dx.size() == 0) {
     314          15 :     dx.resize(this->d_, 0.0);
     315             :   }
     316          16 :   if(dx.size() != this->d_) {
     317           0 :     error("Number of arguments does not match number of KERNEL_DX parameters");
     318             :   }
     319          52 :   for(unsigned i = 0; i < this->d_; ++i) {
     320          36 :     if(this->sketch_conv_ && w[i] <= 0.0) {
     321           0 :       error("Gaussian smoothing requires positive WIDTH");
     322             :     }
     323          36 :     if(kernel && dx[i] < 0.0) {
     324           0 :       error("Kernel basis requires positive KERNEL_DX");
     325             :     }
     326          36 :     if(interval_max[i] <= interval_min[i]) {
     327           0 :       error("INTERVAL_MAX parameters need to be greater than respective INTERVAL_MIN parameters");
     328             :     }
     329          36 :     if(getPntrToArgument(i)->isPeriodic()) {
     330             :       // the basis is periodic over the interval: for a periodic CV the two periods should coincide
     331             :       double pmin, pmax;
     332          24 :       getPntrToArgument(i)->getDomain(pmin, pmax);
     333          24 :       const double tol = 1e-4 * (pmax - pmin);
     334          24 :       if(std::abs(interval_min[i] - pmin) > tol || std::abs(interval_max[i] - pmax) > tol) {
     335           0 :         warning("the interval of the periodic argument " + getPntrToArgument(i)->getName() +
     336             :                 " does not coincide with its periodicity domain: the bias will be discontinuous");
     337             :       }
     338             :     }
     339          72 :     this->sketch_basis_.push_back(BasisFunc(std::make_pair(interval_min[i], interval_max[i]), nbasis, w[i], kernel, dx[i]));
     340             :   }
     341          16 :   if(kernel && this->sketch_conv_) {
     342           0 :     error("kernel smoothing incompatible with kernel basis");
     343             :   }
     344          16 :   this->imin_ = interval_min;
     345          16 :   this->imax_ = interval_max;
     346          16 :   if(kernel) {
     347           4 :     for(unsigned i = 0; i < this->d_; ++i) {
     348           3 :       const BasisFunc& b = this->sketch_basis_[i];
     349           3 :       log.printf("  kernel basis of argument %s: %d kernels of width %g, Gram matrix condition number %.1e, %d nearly dependent direction(s) left out\n",
     350             :                  getPntrToArgument(i)->getName().c_str(), nbasis - 1, b.dx(), b.gramCondition(), b.gramDropped());
     351           3 :       if(b.gramDropped() > 1) {
     352           0 :         warning("the kernel basis of argument " + getPntrToArgument(i)->getName() +
     353           0 :                 " is nearly linearly dependent (" + std::to_string(b.gramDropped()) +
     354             :                 " directions left out): KERNEL_DX is too large for SKETCH_NBASIS");
     355             :       }
     356             :     }
     357             :   }
     358          16 :   if(this->walkers_mpi_) {
     359           4 :     this->mpi_size_ = getNumberOfReplicas();
     360           4 :     this->mpi_rank_ = getReplicaIndex();
     361             :   }
     362             : 
     363          16 :   parse("SKETCH_UNTIL", this->sketch_until_);
     364             : 
     365          16 :   parseFlag("NONINTRUSIVE", this->nonintrusive_);
     366          32 :   parseFlag("DETERMINISTIC", this->deterministic_);
     367             : 
     368          16 :   if(getRestart()) {
     369             :     // the last TT in the file is the bias; the hills file holds the hills deposited after it
     370           3 :     const unsigned ntt = ttCount(this->ttfilename_);
     371           3 :     if(ntt > 0) {
     372           3 :       this->sketch_count_ = ntt + 1;
     373           3 :       setVb(ttRead(this->ttfilename_, this->sketch_count_));
     374           3 :       if(length(this->vb_) != static_cast<int>(this->d_)) {
     375           0 :         error("the TT in " + this->ttfilename_ + " has " + std::to_string(length(this->vb_)) +
     376           0 :               " dimensions, but TTMETAD has " + std::to_string(this->d_) + " arguments");
     377             :       }
     378           9 :       for(unsigned i = 0; i < this->d_; ++i) {
     379           6 :         if(this->vbDense_.nbasis(i) != this->sketch_basis_[i].nbasis()) {
     380           0 :           error("the TT in " + this->ttfilename_ + " has " + std::to_string(this->vbDense_.nbasis(i)) +
     381           0 :                 " basis functions along argument " + std::to_string(i + 1) + ", but SKETCH_NBASIS gives " +
     382           0 :                 std::to_string(this->sketch_basis_[i].nbasis()));
     383             :         }
     384             :       }
     385           3 :       if(this->nonintrusive_ && !ttMomentsRead(this->ttfilename_, this->sketch_count_, this->d_ - 1, this->B_prev_, this->A_prev_)) {
     386           0 :         error("NONINTRUSIVE restart needs the tensor moments of the last sketch, which are not in " + this->ttfilename_);
     387             :       }
     388             :     }
     389           3 :     log << "  restarting from TT " << this->sketch_count_ - 1 << " in " << this->ttfilename_ << "\n";
     390           3 :     IFile hills_ifile;
     391           3 :     hills_ifile.link(*this);
     392           3 :     if(this->walkers_mpi_) {
     393           0 :       hills_ifile.enforceSuffix("");
     394             :     }
     395           3 :     if(!hills_ifile.FileExist(this->hillsfname_)) {
     396           0 :       error("The hills file cannot be found");
     397             :     }
     398           3 :     hills_ifile.open(this->hillsfname_);
     399           3 :     readGaussians(&hills_ifile);
     400           3 :     hills_ifile.close();
     401           3 :   } else if(writerReplica()) {
     402             :     // a fresh run: keep a TT file left by a previous run as a backup, as for the other output files,
     403             :     // so that it cannot be mistaken for the TTs of this run at a later restart
     404          11 :     OFile backup;
     405          11 :     backup.link(*this);
     406          11 :     backup.backupFile("bck", this->ttfilename_);
     407          11 :   }
     408             : 
     409          16 :   log << "  TTs are stored in " << this->ttfilename_ << "\n";
     410          16 :   log << "  Bibliography " << plumed.cite("Strand, Yang, Khoo, and Dinner, https://arxiv.org/abs/2603.13549 (2026)");
     411          16 :   log << plumed.cite("Fishman, White, and Stoudenmire, https://arxiv.org/abs/2007.14822 (2020)");
     412          16 :   log << "\n";
     413             : 
     414             :   if(writerReplica()) {
     415          14 :     this->hillsOfile_.link(*this);
     416          14 :     if(this->walkers_mpi_) {
     417             :       // a single hills file shared by all the walkers (written by the first one);
     418             :       // otherwise each replica has its own, with the replica suffix
     419           4 :       this->hillsOfile_.enforceSuffix("");
     420             :     }
     421          14 :     this->hillsOfile_.open(this->hillsfname_);
     422          14 :     if(this->fmt_.length() > 0) {
     423          14 :       this->hillsOfile_.fmtField(this->fmt_);
     424             :     }
     425             :     hillsOfile_.setHeavyFlush();
     426          46 :     for(unsigned i = 0; i < this->d_; ++i) {
     427          32 :       hillsOfile_.setupPrintValue(getPntrToArgument(i));
     428             :     }
     429             :   }
     430          16 : }
     431             : 
     432           3 : void TTMetaD::readGaussians(IFile *ifile) {
     433           3 :   std::vector<double> center(this->d_);
     434           3 :   std::vector<double> sigma(this->d_);
     435             :   double height;
     436           3 :   int nhills = 0;
     437             : 
     438           3 :   std::vector<Value> tmpvalues;
     439           9 :   for(unsigned j = 0; j < this->d_; ++j) {
     440          18 :     tmpvalues.push_back(Value(this, getPntrToArgument(j)->getName(), false));
     441             :   }
     442             : 
     443           5 :   while(scanOneHill(ifile, tmpvalues, center, sigma, height)) {
     444           2 :     ++nhills;
     445           2 :     if(this->welltemp_ && this->biasf_ > 1.0) {
     446           1 :       height *= (this->biasf_ - 1.0) / this->biasf_;
     447             :     }
     448           4 :     this->hills_.push_back(Gaussian(height, center, sigma));
     449             :   }
     450           3 :   log << "  " << nhills << " hills retrieved\n";
     451           6 : }
     452             : 
     453          50 : void TTMetaD::setVb(const MPS& vb) {
     454          50 :   this->vb_ = vb;
     455          50 :   this->vbDense_ = length(vb) == 0 ? DenseTT() : DenseTT(vb);
     456          50 : }
     457             : 
     458         376 : void TTMetaD::clampToInterval(std::vector<double>& cv, std::vector<char>& out) const {
     459         376 :   out.assign(this->d_, 0);
     460        1232 :   for(unsigned i = 0; i < this->d_; ++i) {
     461         856 :     if(cv[i] < this->imin_[i]) {
     462           0 :       cv[i] = this->imin_[i];
     463           0 :       out[i] = 1;
     464         856 :     } else if(cv[i] > this->imax_[i]) {
     465           8 :       cv[i] = this->imax_[i];
     466           8 :       out[i] = 1;
     467             :     }
     468             :   }
     469         376 : }
     470             : 
     471           5 : bool TTMetaD::scanOneHill(IFile* ifile, std::vector<Value>& tmpvalues, std::vector<double>& center, std::vector<double>& sigma, double& height) {
     472             :   double dummy;
     473          10 :   if(ifile->scanField("time", dummy)) {
     474           2 :     unsigned ncv = tmpvalues.size();
     475           6 :     for(unsigned i = 0; i < ncv; ++i) {
     476           4 :       ifile->scanField(&tmpvalues[i]);
     477           4 :       if(tmpvalues[i].isPeriodic() && !getPntrToArgument(i)->isPeriodic()) {
     478           0 :         error("in hills file periodicity for variable " + tmpvalues[i].getName() + " does not match periodicity in input");
     479           4 :       } else if(tmpvalues[i].isPeriodic()) {
     480             :         std::string imin, imax;
     481           3 :         tmpvalues[i].getDomain(imin, imax);
     482             :         std::string rmin, rmax;
     483           3 :         getPntrToArgument(i)->getDomain(rmin, rmax);
     484           3 :         if(imin != rmin || imax != rmax) {
     485           0 :           error("in hills file periodicity for variable " + tmpvalues[i].getName() + " does not match periodicity in input");
     486             :         }
     487             :       }
     488           4 :       center[i] = tmpvalues[i].get();
     489             :     }
     490           6 :     for(unsigned i = 0; i < ncv; ++i) {
     491           8 :       ifile->scanField("sigma_" + getPntrToArgument(i)->getName(), sigma[i]);
     492             :     }
     493             : 
     494           2 :     ifile->scanField("height", height);
     495           2 :     ifile->scanField("biasf", dummy);
     496           2 :     ifile->scanField();
     497           2 :     return true;
     498             :   } else {
     499             :     return false;
     500             :   }
     501             : }
     502             : 
     503         273 : void TTMetaD::writeGaussian(const Gaussian& hill, OFile&file) {
     504         273 :   file.printField("time", getTimeStep() * getStep());
     505         889 :   for(unsigned i = 0; i < this->d_; ++i) {
     506         616 :     file.printField(getPntrToArgument(i), hill.center[i]);
     507             :   }
     508         889 :   for(unsigned i = 0; i < this->d_; ++i) {
     509        1232 :     file.printField("sigma_" + getPntrToArgument(i)->getName(), hill.sigma[i]);
     510             :   }
     511         273 :   double height = hill.height;
     512         273 :   if(this->welltemp_ && this->biasf_ > 1.0) {
     513          40 :     height *= this->biasf_ / (this->biasf_ - 1.0);
     514             :   }
     515         546 :   file.printField("height", height).printField("biasf", this->biasf_);
     516         273 :   file.printField();
     517         273 : }
     518             : 
     519             : // Called every MD step: evaluate total bias (TT + remaining Gaussians) and set forces.
     520         336 : void TTMetaD::calculate() {
     521         336 :   std::vector<double> cv(this->d_);
     522        1092 :   for(unsigned i = 0; i < this->d_; ++i) {
     523         756 :     cv[i] = getArgument(i);
     524             :   }
     525             : 
     526             :   // outside the interval the bias is the one at the closest boundary and the force along that CV is zero
     527         336 :   std::vector<char> out;
     528         336 :   clampToInterval(cv, out);
     529             : 
     530         336 :   std::vector<double> der(this->d_, 0.0);
     531             : 
     532         336 :   double ene = getBiasAndDerivatives(cv, der);
     533        1092 :   for(unsigned i = 0; i < this->d_; ++i) {
     534         756 :     if(out[i]) {
     535           4 :       der[i] = 0.0;
     536             :     }
     537             :   }
     538         336 :   setBias(ene);
     539        1092 :   for(unsigned i = 0; i < this->d_; ++i) {
     540         756 :     setOutputForce(i, -der[i]);
     541             :   }
     542         672 : }
     543             : 
     544             : // Called every MD step after forces are applied.
     545             : // Two separate triggers:
     546             : //   1. Every sketch_stride_ steps: run paraSketch() to compress accumulated hills into vb_,
     547             : //      clear the hills list, and rewind the HILLS file for the next interval.
     548             : //   2. Every stride_ steps: deposit a new Gaussian hill and write it to the HILLS file.
     549         336 : void TTMetaD::update() {
     550             :   // SKETCH_UNTIL: at the first step after this time the pending hills are sketched
     551             :   // one last time and the bias is frozen
     552         336 :   const bool freezeNow = !this->frozen_ && !this->isFirstStep_ && getTime() >= this->sketch_until_;
     553             :   // all the walkers hold the same list of hills, so they all take the same decision here
     554         336 :   const bool nowAddATT = !this->frozen_ && !this->isFirstStep_ && !this->hills_.empty() &&
     555         261 :                          (getStep() % this->sketch_stride_ == 0 || freezeNow);
     556             : 
     557             :   if(nowAddATT) {
     558             :     if(writerReplica()) {
     559          43 :       this->hillsOfile_.flush();
     560          43 :       unsigned N = this->hills_.size();
     561          43 :       log << "Sample limits\n";
     562         150 :       for(unsigned i = 0; i < this->d_; ++i) {
     563         107 :         auto [large, small] = this->sketch_basis_[i].dom();
     564         107 :         unsigned nout = 0;
     565         695 :         for(unsigned j = 0; j < N; ++j) {
     566         588 :           const double c = this->hills_[j].center[i];
     567         588 :           if(c < this->imin_[i] || c > this->imax_[i]) {
     568           4 :             ++nout;
     569             :           }
     570         588 :           large = std::max(large, c);
     571         588 :           small = std::min(small, c);
     572             :         }
     573         107 :         log << small << " " << large << "\n";
     574         107 :         if(nout > 0) {
     575           3 :           log << "  WARNING: " << nout << " hills are centered outside the interval of argument " << getPntrToArgument(i)->getName()
     576           3 :               << ": the periodic basis folds them back into the interval\n";
     577             :         }
     578             :       }
     579             : 
     580          43 :       log << "\nEmpirical means:\n";
     581          43 :       std::vector<double> muhat(this->d_, 0.0);
     582         150 :       for(unsigned k = 0; k < this->d_; ++k) {
     583         695 :         for(unsigned j = 0; j < N; ++j) {
     584         588 :           muhat[k] += this->hills_[j].center[k] / N;
     585             :         }
     586         107 :         log << muhat[k] << " ";
     587             :       }
     588          43 :       if(N > 1) {
     589          33 :         log << "\nEmpirical covariance matrix:\n";
     590          33 :         Matrix<double> sigmahat(this->d_, this->d_);
     591         111 :         for(unsigned k = 0; k < this->d_; ++k) {
     592         213 :           for(unsigned l = k; l < this->d_; ++l) {
     593             :             double sum = 0.0;
     594        1059 :             for(unsigned j = 0; j < N; ++j) {
     595         924 :               sum += (this->hills_[j].center[k] - muhat[k]) * (this->hills_[j].center[l] - muhat[l]);
     596             :             }
     597         135 :             sigmahat(k, l) = sigmahat(l, k) = sum / (N - 1);
     598             :           }
     599             :         }
     600          33 :         matrixOut(log, sigmahat);
     601             :       } else {
     602          10 :         log << "\n";
     603             :       }
     604             : 
     605             :       // record current bias at hill centers before update, for computing relative error after
     606          43 :       std::vector<std::vector<double>> x(N);
     607          43 :       std::vector<double> A0(N);
     608         304 :       for(unsigned i = 0; i < N; ++i) {
     609         261 :         x[i] = this->hills_[i].center;
     610         261 :         A0[i] = getBias(x[i]);
     611             :       }
     612             : 
     613          43 :       log << "\nStarting TT-sketch...\n";
     614          43 :       log.flush();
     615          43 :       paraSketch();
     616          43 :       ++this->sketch_count_;
     617             : 
     618          43 :       this->hills_.clear();
     619             : 
     620             :       // compute relative L2 approximation error at hill centers: ||V_new - V_old|| / ||V_old||
     621          43 :       std::vector<double> diff(N);
     622         304 :       for(unsigned i = 0; i < N; ++i) {
     623         261 :         diff[i] = getBias(x[i]) - A0[i];
     624             :       }
     625          43 :       log << "Relative l2 error = " << sqrt(norm(diff) / norm(A0)) << "\n\n";
     626          43 :       log.flush();
     627             : 
     628             :       // all ranks of the replica hold the same vb_; one of them writes it
     629          43 :       if(comm.Get_rank() == 0) {
     630          41 :         ttWrite(this->ttfilename_, this->vb_, this->sketch_count_);
     631          41 :         if(this->nonintrusive_) {
     632          12 :           ttMomentsWrite(this->ttfilename_, this->B_prev_, this->A_prev_, this->sketch_count_);
     633             :         }
     634             :       }
     635          43 :     }
     636             : 
     637          47 :     if(this->walkers_mpi_) {
     638           8 :       if(comm.Get_rank() == 0) {
     639           4 :         multi_sim_comm.Bcast(this->sketch_count_, 0);
     640             :       }
     641           8 :       comm.Bcast(this->sketch_count_, 0);
     642           8 :       if(this->mpi_rank_ != 0) {
     643           4 :         this->hills_.clear();
     644           4 :         setVb(ttRead(this->ttfilename_, this->sketch_count_));
     645             :       }
     646             :     }
     647             :     // the hills are now in the TT: the hills file restarts empty
     648             :     if(writerReplica()) {
     649          43 :       this->hillsOfile_.rewind();
     650          43 :       this->hillsOfile_.clearFields();
     651          43 :       if(this->fmt_.length() > 0) {
     652          43 :         this->hillsOfile_.fmtField(this->fmt_);
     653             :       }
     654             :       hillsOfile_.setHeavyFlush();
     655         150 :       for(unsigned i = 0; i < this->d_; ++i) {
     656         107 :         hillsOfile_.setupPrintValue(getPntrToArgument(i));
     657             :       }
     658             :     }
     659             :   }
     660         336 :   if(freezeNow) {
     661           2 :     this->frozen_ = true;
     662           2 :     log << "TT bias frozen at time " << getTime() << "\n";
     663             :   }
     664             : 
     665             :   bool nowAddAHill;
     666         336 :   if(getStep() % this->stride_ == 0 && !isFirstStep_ && !this->frozen_) {
     667             :     nowAddAHill = true;
     668             :   } else {
     669             :     nowAddAHill = false;
     670          63 :     this->isFirstStep_ = false;
     671             :   }
     672             : 
     673         336 :   std::vector<double> cv(this->d_);
     674        1092 :   for(unsigned i = 0; i < this->d_; ++i) {
     675         756 :     cv[i] = getArgument(i);
     676             :   }
     677             : 
     678         336 :   if(nowAddAHill) {
     679         273 :     double height = getHeight(cv);
     680             : 
     681         273 :     if(this->walkers_mpi_) {
     682          80 :       std::vector<double> all_cv(this->mpi_size_ * this->d_, 0.0);
     683          80 :       std::vector<double> all_sigma(this->mpi_size_ * this->sigma0_.size(), 0.0);
     684          80 :       std::vector<double> all_height(this->mpi_size_, 0.0);
     685          80 :       if(comm.Get_rank() == 0) {
     686          40 :         multi_sim_comm.Allgather(cv, all_cv);
     687          40 :         multi_sim_comm.Allgather(this->sigma0_, all_sigma);
     688          40 :         multi_sim_comm.Allgather(height * (this->biasf_ > 1.0 ? this->biasf_ / (this->biasf_ - 1.0) : 1.0), all_height);
     689             :       }
     690          80 :       comm.Bcast(all_cv, 0);
     691          80 :       comm.Bcast(all_sigma, 0);
     692          80 :       comm.Bcast(all_height, 0);
     693             : 
     694         240 :       for(int i = 0; i < this->mpi_size_; i++) {
     695         160 :         std::vector<double> cv_now(this->d_);
     696         160 :         std::vector<double> sigma_now(this->sigma0_.size());
     697         480 :         for(unsigned j = 0; j < this->d_; j++) {
     698         320 :           cv_now[j] = all_cv[i * this->d_ + j];
     699             :         }
     700         480 :         for(unsigned j = 0; j < this->sigma0_.size(); j++) {
     701         320 :           sigma_now[j] = all_sigma[i * this->sigma0_.size() + j];
     702             :         }
     703         160 :         double fact = (this->biasf_ > 1.0 ? (this->biasf_ - 1.0) / this->biasf_ : 1.0);
     704         160 :         Gaussian newhill(all_height[i] * fact, cv_now, sigma_now);
     705         160 :         this->hills_.push_back(newhill);
     706         160 :         if(this->mpi_rank_ == 0) {
     707          80 :           writeGaussian(newhill, hillsOfile_);
     708             :         }
     709         160 :       }
     710             :     } else {
     711         193 :       Gaussian newhill(height, cv, this->sigma0_);
     712         193 :       this->hills_.push_back(newhill);
     713         193 :       writeGaussian(newhill, hillsOfile_);
     714         193 :     }
     715             :   }
     716             : 
     717         336 :   if(getStep() % this->sketch_stride_ == 1 && !this->frozen_) {
     718           0 :     log << "Vbias update " << this->sketch_count_ << "...\n\n";
     719           0 :     log.flush();
     720             :   }
     721         336 : }
     722             : 
     723         273 : double TTMetaD::getHeight(const std::vector<double>& cv) {
     724         273 :   double height = this->height0_;
     725         273 :   if(this->welltemp_) {
     726             :     // the bias felt by the system (see calculate())
     727          40 :     std::vector<double> cvc(cv);
     728          40 :     std::vector<char> out;
     729          40 :     clampToInterval(cvc, out);
     730          40 :     double vbias = getBias(cvc);
     731          40 :     if(this->biasf_ > 1.0) {
     732          40 :       height = this->height0_ * exp(-vbias / (this->kbt_ * (this->biasf_ - 1.0)));
     733             :     } else {
     734           0 :       height = this->height0_ * exp(-vbias / this->kbt_);
     735             :     }
     736          40 :   }
     737         273 :   return height;
     738             : }
     739             : 
     740             : // Total bias = TT approximation of accumulated hills + sum of recently deposited Gaussians.
     741         562 : double TTMetaD::getBias(const std::vector<double>& cv) {
     742         562 :   double bias = this->vbDense_.empty() ? 0.0 : this->vbDense_.eval(this->sketch_basis_, cv, this->sketch_conv_, nullptr);
     743         562 :   unsigned nt = OpenMP::getNumThreads();
     744         562 :   #pragma omp parallel num_threads(nt)
     745             :   {
     746             :     #pragma omp for reduction(+:bias) nowait
     747             :     for(unsigned i = 0; i < hills_.size(); ++i) {
     748             :       bias += evaluateGaussian(cv, this->hills_[i]);
     749             :     }
     750             :   }
     751         562 :   return bias;
     752             : }
     753             : 
     754         336 : double TTMetaD::getBiasAndDerivatives(const std::vector<double>& cv, std::vector<double>& der) {
     755             :   double bias = 0.0;
     756         336 :   if (!this->vbDense_.empty()) {
     757         227 :     bias = this->vbDense_.eval(this->sketch_basis_, cv, this->sketch_conv_, &der);
     758             :   } else {
     759         109 :     der.assign(this->d_, 0.0);
     760             :   }
     761         336 :   unsigned nt = OpenMP::getNumThreads();
     762         336 :   if(this->hills_.size() < 2 * nt || nt == 1) {
     763         179 :     std::vector<double> dp(this->d_);
     764         378 :     for(unsigned i = 0; i < this->hills_.size(); ++i) {
     765         199 :       bias += evaluateGaussianAndDerivatives(cv, this->hills_[i], der, dp);
     766             :     }
     767             :   } else {
     768         157 :     #pragma omp parallel num_threads(nt)
     769             :     {
     770             :       std::vector<double> omp_deriv(this->d_, 0.0);
     771             :       std::vector<double> dp(this->d_);
     772             :       #pragma omp for reduction(+:bias) nowait
     773             :       for(unsigned i = 0; i < this->hills_.size(); ++i) {
     774             :         bias += evaluateGaussianAndDerivatives(cv, this->hills_[i], omp_deriv, dp);
     775             :       }
     776             :       #pragma omp critical
     777             :       for(unsigned i = 0; i < this->d_; ++i) {
     778             :         der[i] += omp_deriv[i];
     779             :       }
     780             :     }
     781             :   }
     782         336 :   return bias;
     783             : }
     784             : 
     785        2795 : double TTMetaD::evaluateGaussian(const std::vector<double>& cv, const Gaussian& hill) {
     786             :   double dp2 = 0.0;
     787        8703 :   for(unsigned i = 0; i < this->d_; i++) {
     788        5908 :     double dp = difference(i, hill.center[i], cv[i]) * hill.invsigma[i];
     789        5908 :     dp2 += dp * dp;
     790             :   }
     791        2795 :   dp2 *= 0.5;
     792             : 
     793             :   double bias = 0.0;
     794        2795 :   if(dp2 < dp2cutoff) {
     795        2049 :     bias = hill.height * exp(-dp2);
     796             :   }
     797             : 
     798        2795 :   return bias;
     799             : }
     800             : 
     801        1505 : double TTMetaD::evaluateGaussianAndDerivatives(const std::vector<double>& cv, const Gaussian& hill, std::vector<double>& der, std::vector<double>& dp) {
     802             :   double dp2 = 0.0;
     803             :   double bias = 0.0;
     804        4689 :   for(unsigned i = 0; i < this->d_; i++) {
     805        3184 :     dp[i] = difference(i, hill.center[i], cv[i]) * hill.invsigma[i];
     806        3184 :     dp2 += dp[i] * dp[i];
     807             :   }
     808        1505 :   dp2 *= 0.5;
     809        1505 :   if(dp2 < dp2cutoff) {
     810        1090 :     bias = hill.height * exp(-dp2);
     811        3369 :     for(unsigned i = 0; i < this->d_; i++) {
     812        2279 :       der[i] -= bias * dp[i] * hill.invsigma[i];
     813             :     }
     814             :   }
     815             : 
     816        1505 :   return bias;
     817             : }
     818             : 
     819             : // TT-Sketch algorithm: compresses the N accumulated Gaussian hills plus the previous
     820             : // TT bias vb_ into a new low-rank TT approximation G, which becomes the updated vb_.
     821             : //
     822             : // Overview (see paper Algorithm 2):
     823             : //   1. createTTCoeff(): build a random sketch coefficient TT `coeff` of rank sketch_rc_.
     824             : //   2. intBasisSample(): compute M[i](j,k) = <phi_k, g_j>_{dim i}, the inner product of
     825             : //      each basis function with each Gaussian along dimension i.
     826             : //   3. formTensorMoment(): contract coeff with M to get the tensor moment Bemp and the
     827             : //      cross-gram matrices A of the bonds (normal equations).
     828             : //   4. For each bond between cores k and k+1:
     829             : //      - Add the contribution of the previous bias (or of the previous moments) to A.
     830             : //      - SVD A to get the rank-trimmed projection V (truncating to sketch_r_ or sketch_cutoff_).
     831             : //   5. Recover TT cores G from Bemp and the pseudo-inverse of U*S.
     832             : //   6. Apply Gram matrix correction (kernel basis only) to convert to dual basis coefficients.
     833          43 : void TTMetaD::paraSketch() {
     834          43 :   auto coeff = createTTCoeff();
     835          43 :   auto M = intBasisSample();
     836          43 :   MPS G(this->d_);
     837             : 
     838          43 :   auto [Bemp, AMats] = formTensorMoment(M, coeff);
     839          43 :   MPS Bemp_Vb;
     840          43 :   std::vector<ITensor> envi_L_Vb;
     841          43 :   std::vector<ITensor> envi_R_Vb;
     842          43 :   if(this->sketch_count_ != 1) {
     843          32 :     if(this->nonintrusive_) {
     844             :       // Non-intrusive variant: accumulate tensor moments directly across sketches.
     845             :       // B_prev_ stores the sum of Bemp from all prior intervals; re-index its bond/site
     846             :       // indices to match the current coeff TT's indices before adding.
     847          10 :       auto sites = siteInds(Bemp);
     848          10 :       auto sites_prev = siteInds(this->B_prev_);
     849          10 :       auto links = linkInds(Bemp);
     850          10 :       auto links_prev = linkInds(this->B_prev_);
     851          33 :       for(unsigned i = 1; i <= this->d_; ++i) {
     852          23 :         this->B_prev_.ref(i) *= delta(sites(i), sites_prev(i));
     853          23 :         if(i != 1) {
     854          13 :           this->B_prev_.ref(i) *= delta(links(i - 1), links_prev(i - 1));
     855             :         }
     856          23 :         if (i != this->d_) {
     857          13 :           this->B_prev_.ref(i) *= delta(links(i), links_prev(i));
     858             :         }
     859          23 :         Bemp.ref(i) += this->B_prev_(i);
     860             :       }
     861             :     } else {
     862             :       // Intrusive variant: project the previous TT bias vb_ onto the current sketch's
     863             :       // basis by pre-multiplying each core by the Gram matrix (kernel basis) or the
     864             :       // convolution inner-product matrix (Fourier conv mode), then add its tensor moment.
     865          22 :       if(this->sketch_basis_[0].kernel()) {
     866             :         // Convert vb_ from dual basis to standard basis by applying Gram matrix per core
     867          32 :         for(unsigned i = 1; i <= this->d_; ++i) {
     868          24 :           auto s = siteIndex(this->vb_, i);
     869          24 :           ITensor gram(s, prime(s));
     870         216 :           for(int j = 1; j <= dim(s); ++j) {
     871        1728 :             for(int l = 1; l <= dim(s); ++l) {
     872        4608 :               gram.set(s = j, prime(s) = l, this->sketch_basis_[i - 1].gram()(j - 1, l - 1));
     873             :             }
     874             :           }
     875          24 :           this->vb_.ref(i) *= gram;
     876          24 :           this->vb_.ref(i).noPrime();
     877          24 :         }
     878             :       }
     879             :       // Note: in SKETCH_WIDTH (conv) mode the coefficients of vb_ are those of the unsmoothed bias,
     880             :       // the smoothing being applied when the basis is evaluated, so they are added as they are.
     881             :       // Damping them here as well (as done before) smoothed the old bias again at every sketch,
     882             :       // so that the intrusive variant did not match NONINTRUSIVE even when the TT is exact.
     883          22 :       auto vbresult = formTensorMomentVb(coeff);
     884          22 :       Bemp_Vb = std::get<0>(vbresult);
     885          22 :       envi_L_Vb = std::get<1>(vbresult);
     886          22 :       envi_R_Vb = std::get<2>(vbresult);
     887          80 :       for(unsigned i = 1; i <= this->d_; ++i) {
     888          58 :         Bemp.ref(i) += Bemp_Vb(i);
     889             :       }
     890             :     }
     891             :   }
     892          43 :   if(this->nonintrusive_) {
     893          12 :     this->B_prev_ = Bemp;
     894          12 :     if(this->sketch_count_ == 1) {
     895           2 :       this->A_prev_.resize(this->d_ - 1);
     896             :     }
     897             :   }
     898             :   // For each bond between cores k-1 and k, form the cross-gram matrix A = L^T * R,
     899             :   // where L = envi_L[k] and R = envi_R[k-2] are the left/right environment projections
     900             :   // of coeff onto the sample indices. SVD A to find the rank-trimmed subspace V.
     901          43 :   auto links = linkInds(coeff);
     902          43 :   std::vector<ITensor> U(this->d_), S(this->d_), V(this->d_);
     903          43 :   std::vector<Index> links_trimmed;
     904         107 :   for(unsigned core_id = 2; core_id <= this->d_; ++core_id) {
     905          64 :     int rank = dim(links(core_id - 1));
     906          64 :     const Matrix<double>& AMat = AMats[core_id - 2];
     907          64 :     Matrix<double> LMat, RMat, Lt, AMat_Vb;
     908             : 
     909          64 :     if(this->sketch_count_ != 1 && !this->nonintrusive_) {
     910          36 :       auto ivb = linkIndex(this->vb_, core_id - 1);
     911          36 :       int rank_vb = dim(ivb);
     912          36 :       LMat = Matrix<double>(rank_vb, rank);
     913          36 :       RMat = Matrix<double>(rank_vb, rank);
     914         406 :       for(int i = 1; i <= rank_vb; ++i) {
     915       18438 :         for(int j = 1; j <= rank; ++j) {
     916       18068 :           LMat(i - 1, j - 1) = envi_L_Vb[core_id - 1].elt(ivb = i, links(core_id - 1) = j);
     917       18068 :           RMat(i - 1, j - 1) = envi_R_Vb[core_id - 2].elt(ivb = i, links(core_id - 1) = j);
     918             :         }
     919             :       }
     920          36 :       transpose(LMat, Lt);
     921          36 :       mult(Lt, RMat, AMat_Vb);
     922             :     }
     923             : 
     924          64 :     ITensor A(prime(links(core_id - 1)), links(core_id - 1));
     925        1288 :     for(int i = 1; i <= rank; ++i) {
     926       60312 :       for(int j = 1; j <= rank; ++j) {
     927      118176 :         A.set(prime(links(core_id - 1)) = i, links(core_id - 1) = j, AMat(i - 1, j - 1));
     928             :       }
     929             :     }
     930          64 :     if(this->sketch_count_ != 1) {
     931          49 :       if(this->nonintrusive_) {
     932          13 :         this->A_prev_[core_id - 2] *= delta(this->A_prev_[core_id - 2].index(1), A.index(1));
     933          13 :         this->A_prev_[core_id - 2] *= delta(this->A_prev_[core_id - 2].index(2), A.index(2));
     934          13 :         A += this->A_prev_[core_id - 2];
     935             :       } else {
     936          36 :         ITensor A_Vb(prime(links(core_id - 1)), links(core_id - 1));
     937         562 :         for(int i = 1; i <= rank; ++i) {
     938       23066 :           for(int j = 1; j <= rank; ++j) {
     939       45080 :             A_Vb.set(prime(links(core_id - 1)) = i, links(core_id - 1) = j, AMat_Vb(i - 1, j - 1));
     940             :           }
     941             :         }
     942          36 :         A += A_Vb;
     943          36 :       }
     944             :     }
     945          64 :     if(this->nonintrusive_) {
     946          16 :       this->A_prev_[core_id - 2] = A;
     947             :     }
     948          64 :     auto original_link_tags = tags(links(core_id - 1));
     949          64 :     V[core_id - 1] = ITensor(links(core_id - 1));
     950          64 :     if(this->sketch_r_ > 0) {
     951         112 :       svd(A, U[core_id - 1], S[core_id - 1], V[core_id - 1],
     952          56 :       {"Cutoff=", this->sketch_cutoff_, "RightTags=", original_link_tags, "MaxDim=", this->sketch_r_});
     953             :     } else {
     954          16 :       svd(A, U[core_id - 1], S[core_id - 1], V[core_id - 1], {"Cutoff=", this->sketch_cutoff_, "RightTags=", original_link_tags});
     955             :     }
     956          64 :     links_trimmed.push_back(commonIndex(S[core_id - 1], V[core_id - 1]));
     957          64 :   }
     958             : 
     959             :   // Recover TT cores G from Bemp and the trimmed subspaces.
     960             :   // G[1] = Bemp[1] * V[1] (project first core onto rank-trimmed subspace)
     961             :   // G[k] = pinv(U[k]*S[k]) * Bemp[k] * V[k]  for 2 <= k < d
     962             :   // G[d] = pinv(U[d]*S[d]) * Bemp[d]
     963             :   // Since U has orthonormal columns, pinv(U*S) = S^+ * U^T, where S^+ inverts only the
     964             :   // singular values above a relative threshold: the others (e.g. all but one when a single
     965             :   // hill is sketched, so that A has rank one) are rounding errors, and dividing Bemp by them
     966             :   // would fill the TT with noise of order one.
     967          43 :   G.ref(1) = Bemp(1) * V[1];
     968          43 :   std::vector<int> effective_rank(this->d_, 0);
     969         107 :   for(unsigned core_id = 2; core_id <= this->d_; ++core_id) {
     970          64 :     int rank = dim(links(core_id - 1)), rank_trimmed = dim(links_trimmed[core_id - 2]);
     971          64 :     const Index iu = commonIndex(U[core_id - 1], S[core_id - 1]);
     972          64 :     const Index iv = links_trimmed[core_id - 2];
     973          64 :     std::vector<double> sinv(rank_trimmed, 0.0);
     974          64 :     double smax = 0.0;
     975         971 :     for(int j = 1; j <= rank_trimmed; ++j) {
     976         971 :       smax = std::max(smax, S[core_id - 1].elt(iu = j, iv = j));
     977             :     }
     978         971 :     for(int j = 1; j <= rank_trimmed; ++j) {
     979        1814 :       const double sj = S[core_id - 1].elt(iu = j, iv = j);
     980         907 :       if(sj > svdTolerance * smax) {
     981         216 :         sinv[j - 1] = 1.0 / sj;
     982         216 :         ++effective_rank[core_id - 2];
     983             :       }
     984             :     }
     985          64 :     ITensor Pinv(iv, links(core_id - 1));
     986         971 :     for(int i = 1; i <= rank_trimmed; ++i) {
     987       48637 :       for(int j = 1; j <= rank; ++j) {
     988      143190 :         Pinv.set(iv = i, links(core_id - 1) = j, sinv[i - 1] * U[core_id - 1].elt(prime(links(core_id - 1)) = j, iu = i));
     989             :       }
     990             :     }
     991          64 :     G.ref(core_id) = Pinv * Bemp(core_id);
     992          64 :     if(core_id != this->d_) {
     993          21 :       G.ref(core_id) *= V[core_id];
     994             :     }
     995          64 :   }
     996             : 
     997          43 :   log << "Final ranks ";
     998         107 :   for(unsigned i = 1; i < this->d_; ++i) {
     999         128 :     log << dim(linkIndex(G, i)) << " ";
    1000             :   }
    1001          43 :   log << "(effective ";
    1002         107 :   for(unsigned i = 1; i < this->d_; ++i) {
    1003          64 :     log << effective_rank[i - 1] << " ";
    1004             :   }
    1005          43 :   log << ")\n";
    1006          43 :   log.flush();
    1007             : 
    1008          43 :   if(this->sketch_basis_[0].kernel()) {
    1009             :     // Apply pseudo-inverse of the Gram matrix to each core to convert from the
    1010             :     // primal Gram representation back to dual basis coefficients (G^+ * G_core).
    1011          36 :     for(unsigned i = 1; i <= this->d_; ++i) {
    1012          27 :       auto s = siteIndex(G, i);
    1013          27 :       ITensor ginv(s, prime(s));
    1014         243 :       for(int j = 1; j <= dim(s); ++j) {
    1015        1944 :         for(int l = 1; l <= dim(s); ++l) {
    1016        5184 :           ginv.set(s = j, prime(s) = l, this->sketch_basis_[i - 1].ginv()(j - 1, l - 1));
    1017             :         }
    1018             :       }
    1019          27 :       G.ref(i) *= ginv;
    1020          27 :       G.ref(i).noPrime();
    1021          27 :     }
    1022             :   }
    1023             : 
    1024          43 :   setVb(G);
    1025         129 : }
    1026             : 
    1027             : // Build the random sketch coefficient TT `coeff` of bond dimension sketch_rc_.
    1028             : // Each element is drawn from N(0,1); then each core is multiplied by a diagonal
    1029             : // matrix diag(1, alpha, alpha, ...) so the constant basis function (pos=1) has
    1030             : // full weight while all other harmonics are scaled down by sketch_alpha_. This
    1031             : // ensures the sketch captures the large constant component of the bias accurately.
    1032          43 : MPS TTMetaD::createTTCoeff() const {
    1033          43 :   unsigned seed = this->deterministic_ ? 42u : static_cast<unsigned int>(time(nullptr));
    1034             :   // all ranks of a replica must build the same sketch
    1035          43 :   comm.Bcast(seed, 0);
    1036          43 :   std::default_random_engine generator(seed);
    1037             :   std::normal_distribution<double> distribution(0.0, 1.0);
    1038             :   int n = this->sketch_basis_[0].nbasis();
    1039          43 :   auto sites = SiteSet(this->d_, n);
    1040          43 :   auto coeff = MPS(sites, this->sketch_rc_);
    1041         329 :   for(int j = 1; j <= n; ++j) {
    1042        4906 :     for(int k = 1; k <= this->sketch_rc_; ++k) {
    1043       18480 :       coeff.ref(1).set(sites(1) = j, linkIndex(coeff, 1) = k, distribution(generator));
    1044             :     }
    1045             :   }
    1046          64 :   for(unsigned i = 2; i <= this->d_ - 1; ++i) {
    1047         177 :     for(int j = 1; j <= n; ++j) {
    1048        4116 :       for(int k = 1; k <= this->sketch_rc_; ++k) {
    1049      209160 :         for(int l = 1; l <= this->sketch_rc_; ++l) {
    1050     1026000 :           coeff.ref(i).set(sites(i) = j, linkIndex(coeff, i - 1) = k, linkIndex(coeff, i) = l, distribution(generator));
    1051             :         }
    1052             :       }
    1053             :     }
    1054             :   }
    1055         329 :   for(int j = 1; j <= n; ++j) {
    1056        4906 :     for(int k = 1; k <= this->sketch_rc_; ++k) {
    1057       18480 :       coeff.ref(this->d_).set(sites(this->d_) = j, linkIndex(coeff, this->d_ - 1) = k, distribution(generator));
    1058             :     }
    1059             :   }
    1060         150 :   for(unsigned i = 1; i <= this->d_; ++i) {
    1061         107 :     auto s = sites(i);
    1062         107 :     auto sp = prime(s);
    1063         107 :     std::vector<double> Avec(n, this->sketch_alpha_);
    1064         107 :     Avec[0] = 1.0;
    1065         107 :     auto A = diagITensor(Avec, s, sp);
    1066         107 :     coeff.ref(i) *= A;
    1067         107 :     coeff.ref(i).noPrime();
    1068         107 :   }
    1069          43 :   return coeff;
    1070           0 : }
    1071             : 
    1072             : // Compute the matrix M[dim i][sample j, basis k] = <phi_k, g_j>_{dim i}, the
    1073             : // analytical inner product of each 1D basis function with the 1D marginal of each
    1074             : // Gaussian hill along dimension i. The d-dimensional Gaussian factorizes as a
    1075             : // product of d 1D Gaussians, so we distribute height as h^(1/d) per dimension.
    1076             : // These integrals are computed in closed form:
    1077             : //   - Kernel basis: convolution of the kernel function with the 1D Gaussian hill
    1078             : //   - Fourier basis: Fourier transform of the 1D Gaussian (exponential decay * harmonic)
    1079             : // Returns M as a vector of ITensors (one per dimension) and a new IndexSet is_new
    1080             : // where is_new(i) indexes the N sample points for dimension i.
    1081          43 : std::vector<Matrix<double>> TTMetaD::intBasisSample() const {
    1082          43 :   unsigned N = this->hills_.size();
    1083             :   int nb = this->sketch_basis_[0].nbasis();
    1084          43 :   std::vector<Matrix<double>> M(this->d_, Matrix<double>(N, nb));
    1085         150 :   for(unsigned i = 1; i <= this->d_; ++i) {
    1086         107 :     double L = (this->sketch_basis_[i - 1].dom().second - this->sketch_basis_[i - 1].dom().first) / 2;
    1087         107 :     double a = (this->sketch_basis_[i - 1].dom().second + this->sketch_basis_[i - 1].dom().first) / 2;
    1088         695 :     for(unsigned j = 1; j <= N; ++j) {
    1089         588 :       double x = this->hills_[j - 1].center[i - 1];
    1090         588 :       double w = this->hills_[j - 1].sigma[i - 1];
    1091         588 :       double h = pow(this->hills_[j - 1].height, 1.0 / this->d_);  // per-dimension d-th root of height
    1092        4119 :       for(int pos = 1; pos <= nb; ++pos) {
    1093             :         double result = 0.0;
    1094        3531 :         if(this->sketch_basis_[0].kernel()) {
    1095         216 :           if(pos == 1) {
    1096             :             // <1, g_j> = integral of 1D Gaussian = sqrt(2*pi)*sigma
    1097          27 :             result = h * sqrt(2 * M_PI) * w;
    1098             :           } else {
    1099             :             // <kernel_c, g_j> = conv of two Gaussians (widths dx and w), with periodic images
    1100             :             double c = this->sketch_basis_[i - 1].center(pos - 1);
    1101             :             double dx = this->sketch_basis_[i - 1].dx();
    1102         756 :             for(int k = -1; k <= 1; ++k) {
    1103         567 :               result += exp(-pow(x - c + 2 * k * L, 2) / (2 * (pow(dx, 2) + pow(w, 2)))) * h * sqrt(2 * M_PI) * w /
    1104         567 :                         (sqrt(1 / pow(dx, 2) + 1 / pow(w, 2)) * w);
    1105             :             }
    1106             :           }
    1107             :         } else {
    1108        3315 :           if(pos == 1) {
    1109             :             // <1/sqrt(2L), g_j> = sqrt(pi/L) * sigma (Gaussian times constant)
    1110         561 :             result = h * sqrt(M_PI / L) * w;
    1111        2754 :           } else if(pos % 2 == 0) {
    1112             :             // <cos harmonic, g_j>: Fourier transform of Gaussian times exponential damping
    1113        1377 :             result = exp(-pow(M_PI * w * (pos / 2), 2) / (2 * pow(L, 2))) * h * sqrt(2 * M_PI / L) * w * cos(M_PI * (x - a) * (pos / 2) / L);
    1114             :           } else {
    1115        1377 :             result = exp(-pow(M_PI * w * (pos / 2), 2) / (2 * pow(L, 2))) * h * sqrt(2 * M_PI / L) * w * sin(M_PI * (x - a) * (pos / 2) / L);
    1116             :           }
    1117             :         }
    1118        3531 :         M[i - 1](j - 1, pos - 1) = result;
    1119             :       }
    1120             :     }
    1121             :   }
    1122          43 :   return M;
    1123             : }
    1124             : 
    1125             : // Compute the tensor moment B of the new Gaussian hills and the cross-gram matrices A.
    1126             : //
    1127             : // With K_i(j) = sum_p coeff_i[:, p, :] M_i(j, p) the core i of coeff contracted with the
    1128             : // basis integrals of hill j (a r_{i-1} x r_i matrix), the left and right environments are
    1129             : //   EL_i(j, :) = K_1(j) ... K_i(j)       (i = 1..d-1, size r_i)
    1130             : //   ER_i(j, :) = K_{i+2}(j) ... K_d(j)   (i = 0..d-2, size r_{i+1})
    1131             : // The tensor moment is
    1132             : //   B_1[p, b]    = sum_j ER_0(j, b) M_1(j, p)
    1133             : //   B_k[a, p, b] = sum_j EL_{k-1}(j, a) ER_{k-1}(j, b) M_k(j, p)   (1 < k < d)
    1134             : //   B_d[a, p]    = sum_j EL_{d-1}(j, a) M_d(j, p)
    1135             : // and the cross-gram matrix of bond k = 1..d-1 is A_k = EL_k^T ER_{k-1}.
    1136             : // Everything is done on plain arrays (the environments are independent across hills),
    1137             : // only B is converted to ITensors at the end.
    1138          43 : std::pair<MPS, std::vector<Matrix<double>>> TTMetaD::formTensorMoment(const std::vector<Matrix<double>>& M, const MPS& coeff) const {
    1139          43 :   const unsigned d = this->d_;
    1140             :   const unsigned N = M[0].nrows();
    1141          43 :   const DenseTT C(coeff);
    1142          43 :   const auto sites = siteInds(coeff);
    1143          43 :   const auto links = linkInds(coeff);
    1144          43 :   std::vector<int> r(d + 1), n(d);
    1145         193 :   for(unsigned i = 0; i <= d; ++i) {
    1146         150 :     r[i] = C.rank(i);
    1147             :   }
    1148         150 :   for(unsigned i = 0; i < d; ++i) {
    1149         107 :     n[i] = C.nbasis(i);
    1150             :   }
    1151             :   // EL[i] for i = 1..d-1 (N x r_i), ER[i] for i = 0..d-2 (N x r_{i+1}), row-major
    1152          43 :   std::vector<std::vector<double>> EL(d), ER(d - 1);
    1153         107 :   for(unsigned i = 1; i < d; ++i) {
    1154          64 :     EL[i].assign(static_cast<size_t>(N) * r[i], 0.0);
    1155          64 :     ER[i - 1].assign(static_cast<size_t>(N) * r[i], 0.0);
    1156             :   }
    1157          43 :   const unsigned nt = OpenMP::getNumThreads();
    1158          43 :   #pragma omp parallel num_threads(nt)
    1159             :   {
    1160             :     std::vector<std::vector<double>> K(d);
    1161             :     std::vector<double> tmp;
    1162             :     #pragma omp for schedule(static)
    1163             :     for(unsigned j = 0; j < N; ++j) {
    1164             :       // contracted cores K[i] = K_{i+1}(j), r_i x r_{i+1}
    1165             :       for(unsigned i = 0; i < d; ++i) {
    1166             :         const int rl = r[i], rr = r[i + 1], nb = n[i];
    1167             :         K[i].assign(static_cast<size_t>(rl) * rr, 0.0);
    1168             :         const double* c = C.core(i).data();
    1169             :         for(int a = 0; a < rl; ++a) {
    1170             :           double* k = K[i].data() + static_cast<size_t>(a) * rr;
    1171             :           for(int p = 0; p < nb; ++p) {
    1172             :             const double m = M[i](j, p);
    1173             :             const double* cp = c + (static_cast<size_t>(a) * nb + p) * rr;
    1174             :             for(int b = 0; b < rr; ++b) {
    1175             :               k[b] += cp[b] * m;
    1176             :             }
    1177             :           }
    1178             :         }
    1179             :       }
    1180             :       // left environments
    1181             :       for(int b = 0; b < r[1]; ++b) {
    1182             :         EL[1][static_cast<size_t>(j) * r[1] + b] = K[0][b];
    1183             :       }
    1184             :       for(unsigned i = 2; i < d; ++i) {
    1185             :         const int rl = r[i - 1], rr = r[i];
    1186             :         const double* prev = EL[i - 1].data() + static_cast<size_t>(j) * rl;
    1187             :         double* out = EL[i].data() + static_cast<size_t>(j) * rr;
    1188             :         for(int a = 0; a < rl; ++a) {
    1189             :           const double* k = K[i - 1].data() + static_cast<size_t>(a) * rr;
    1190             :           for(int b = 0; b < rr; ++b) {
    1191             :             out[b] += prev[a] * k[b];
    1192             :           }
    1193             :         }
    1194             :       }
    1195             :       // right environments
    1196             :       for(int a = 0; a < r[d - 1]; ++a) {
    1197             :         ER[d - 2][static_cast<size_t>(j) * r[d - 1] + a] = K[d - 1][a];
    1198             :       }
    1199             :       for(int i = static_cast<int>(d) - 3; i >= 0; --i) {
    1200             :         // ER[i] = K_{i+2} ER[i+1], K_{i+2} = K[i+1] is r_{i+1} x r_{i+2}
    1201             :         const int rl = r[i + 1], rr = r[i + 2];
    1202             :         const double* next = ER[i + 1].data() + static_cast<size_t>(j) * rr;
    1203             :         double* out = ER[i].data() + static_cast<size_t>(j) * rl;
    1204             :         for(int a = 0; a < rl; ++a) {
    1205             :           const double* k = K[i + 1].data() + static_cast<size_t>(a) * rr;
    1206             :           double sum = 0.0;
    1207             :           for(int b = 0; b < rr; ++b) {
    1208             :             sum += k[b] * next[b];
    1209             :           }
    1210             :           out[a] = sum;
    1211             :         }
    1212             :       }
    1213             :     }
    1214             :   }
    1215             : 
    1216             :   // cross-gram matrices
    1217          43 :   std::vector<Matrix<double>> A(d - 1);
    1218         107 :   for(unsigned k = 1; k < d; ++k) {
    1219          64 :     const int rk = r[k];
    1220          64 :     A[k - 1] = Matrix<double>(rk, rk);
    1221          64 :     #pragma omp parallel for num_threads(nt) schedule(static)
    1222             :     for(int a = 0; a < rk; ++a) {
    1223             :       for(int b = 0; b < rk; ++b) {
    1224             :         double sum = 0.0;
    1225             :         for(unsigned j = 0; j < N; ++j) {
    1226             :           sum += EL[k][static_cast<size_t>(j) * rk + a] * ER[k - 1][static_cast<size_t>(j) * rk + b];
    1227             :         }
    1228             :         A[k - 1](a, b) = sum;
    1229             :       }
    1230             :     }
    1231             :   }
    1232             : 
    1233             :   // tensor moment
    1234          43 :   MPS B(d);
    1235             :   {
    1236          43 :     const int nb = n[0], rr = r[1];
    1237          43 :     ITensor core(sites(1), links(1));
    1238         329 :     for(int p = 0; p < nb; ++p) {
    1239        4906 :       for(int b = 0; b < rr; ++b) {
    1240        4620 :         double sum = 0.0;
    1241       26634 :         for(unsigned j = 0; j < N; ++j) {
    1242       22014 :           sum += ER[0][static_cast<size_t>(j) * rr + b] * M[0](j, p);
    1243             :         }
    1244        9240 :         core.set(sites(1) = p + 1, links(1) = b + 1, sum);
    1245             :       }
    1246             :     }
    1247          43 :     B.ref(1) = core;
    1248          43 :   }
    1249          64 :   for(unsigned k = 2; k < d; ++k) {
    1250          21 :     const int rl = r[k - 1], rr = r[k], nb = n[k - 1];
    1251          21 :     std::vector<double> Bk(static_cast<size_t>(rl) * rr * nb, 0.0); // [a][b][p]
    1252          21 :     #pragma omp parallel for num_threads(nt) schedule(static)
    1253             :     for(int a = 0; a < rl; ++a) {
    1254             :       for(unsigned j = 0; j < N; ++j) {
    1255             :         const double la = EL[k - 1][static_cast<size_t>(j) * rl + a];
    1256             :         const double* er = ER[k - 1].data() + static_cast<size_t>(j) * rr;
    1257             :         for(int b = 0; b < rr; ++b) {
    1258             :           const double w = la * er[b];
    1259             :           double* out = Bk.data() + (static_cast<size_t>(a) * rr + b) * nb;
    1260             :           for(int p = 0; p < nb; ++p) {
    1261             :             out[p] += w * M[k - 1](j, p);
    1262             :           }
    1263             :         }
    1264             :       }
    1265             :     }
    1266          21 :     ITensor core(links(k - 1), sites(k), links(k));
    1267         579 :     for(int a = 0; a < rl; ++a) {
    1268       29826 :       for(int b = 0; b < rr; ++b) {
    1269      234468 :         for(int p = 0; p < nb; ++p) {
    1270      410400 :           core.set(links(k - 1) = a + 1, sites(k) = p + 1, links(k) = b + 1, Bk[(static_cast<size_t>(a) * rr + b) * nb + p]);
    1271             :         }
    1272             :       }
    1273             :     }
    1274          21 :     B.ref(k) = core;
    1275          21 :   }
    1276             :   {
    1277          43 :     const int nb = n[d - 1], rl = r[d - 1];
    1278          43 :     ITensor core(links(d - 1), sites(d));
    1279         709 :     for(int a = 0; a < rl; ++a) {
    1280        5286 :       for(int p = 0; p < nb; ++p) {
    1281        4620 :         double sum = 0.0;
    1282       26634 :         for(unsigned j = 0; j < N; ++j) {
    1283       22014 :           sum += EL[d - 1][static_cast<size_t>(j) * rl + a] * M[d - 1](j, p);
    1284             :         }
    1285        9240 :         core.set(links(d - 1) = a + 1, sites(d) = p + 1, sum);
    1286             :       }
    1287             :     }
    1288          43 :     B.ref(d) = core;
    1289          43 :   }
    1290          43 :   return std::make_pair(B, A);
    1291          86 : }
    1292             : 
    1293             : // Compute the tensor moment B and environments for the previous TT bias vb_.
    1294             : // This is the intrusive variant's contribution from the prior accumulated bias.
    1295             : // Analogous to formTensorMoment but uses vb_ in place of M*coeff: the "samples"
    1296             : // are the vb_ bond indices, and the environments are formed by contracting coeff
    1297             : // with vb_ core by core. The result has the same structure as Bemp from the new hills.
    1298          22 : std::tuple<MPS, std::vector<ITensor>, std::vector<ITensor>> TTMetaD::formTensorMomentVb(const MPS& coeff) {
    1299             :   // align vb_ site indices with coeff's site indices for contraction
    1300          80 :   for(unsigned i = 1; i <= this->d_; ++i) {
    1301         116 :     this->vb_.ref(i) *= delta(siteIndex(this->vb_, i), siteIndex(coeff, i));
    1302             :   }
    1303          22 :   std::vector<ITensor> envi_L(this->d_);
    1304          22 :   envi_L[1] = coeff(1) * this->vb_(1);
    1305          36 :   for(unsigned i = 2; i < this->d_; ++i) {
    1306          14 :     envi_L[i] = envi_L[i - 1] * coeff(i) * this->vb_(i);
    1307             :   }
    1308             : 
    1309          22 :   std::vector<ITensor> envi_R(this->d_);
    1310          22 :   envi_R[this->d_ - 2] = coeff(this->d_) * this->vb_(this->d_);
    1311          36 :   for(int i = this->d_ - 3; i >= 0; --i) {
    1312          14 :     envi_R[i] = envi_R[i + 1] * coeff(i + 2) * this->vb_(i + 2);
    1313             :   }
    1314             : 
    1315          22 :   MPS B(this->d_);
    1316          22 :   B.ref(1) = this->vb_(1) * envi_R[0];
    1317          36 :   for(unsigned core_id = 2; core_id < this->d_; ++core_id) {
    1318          14 :     B.ref(core_id) = envi_L[core_id - 1] * this->vb_(core_id) * envi_R[core_id - 1];
    1319             :   }
    1320          22 :   B.ref(this->d_) = envi_L[this->d_ - 1] * this->vb_(this->d_);
    1321             : 
    1322          22 :   return std::make_tuple(B, envi_L, envi_R);
    1323          22 : }
    1324             : 
    1325             : #endif // !defined(__PLUMED_HAS_LIBITENSOR) || !defined(__PLUMED_HAS_LIBHDF5)
    1326             : 
    1327          20 : void TTMetaD::registerKeywords(Keywords& keys) {
    1328          20 :   Bias::registerKeywords(keys);
    1329          20 :   keys.addFlag("KERNEL_BASIS", false, "Specifies that local kernel basis should be used instead of Fourier basis");
    1330          20 :   keys.add("compulsory", "SIGMA", "the widths of the Gaussian hills");
    1331          20 :   keys.add("compulsory", "PACE", "the frequency for hill addition");
    1332          20 :   keys.add("compulsory", "FILE", "HILLS", "a file in which the list of added hills is stored");
    1333          20 :   keys.add("compulsory", "TT_FILE", "ttsketch.h5", "the HDF5 file where the TT bias is stored after every sketch, and read at restart. "
    1334             :            "With multiple replicas the replica suffix is added, with WALKERS_MPI the file is shared and is in the walkers directory");
    1335          20 :   keys.add("compulsory", "HEIGHT", "the heights of the Gaussian hills");
    1336          20 :   keys.add("optional", "FMT", "specify format for HILLS files (useful for decrease the number of digits in regtests)");
    1337          20 :   keys.add("optional", "BIASFACTOR", "use well tempered metadynamics and use this bias factor. Please note you must also specify temp");
    1338          20 :   keys.add("optional", "TEMP", "the system temperature - this is only needed if you are doing well-tempered metadynamics");
    1339          20 :   keys.addFlag("WALKERS_MPI", false, "To be used when gromacs + multiple walkers are used");
    1340          20 :   keys.add("optional", "WALKERS_DIR", "shared directory with the hills files from all the walkers");
    1341          20 :   keys.use("RESTART");
    1342          20 :   keys.add("optional", "SKETCH_RANK", "Target rank for TTSketch algorithm - compulsory if SKETCH_CUTOFF is not specified");
    1343          20 :   keys.add("optional", "SKETCH_CUTOFF", "Truncation error cutoff for singular value decomposition - compulsory if SKETCH_RANK is not specified");
    1344          20 :   keys.add("compulsory", "SKETCH_INITRANK", "Initial rank for TTSketch algorithm");
    1345          20 :   keys.add("compulsory", "SKETCH_PACE", "1e6", "The frequency for TT Vbias updates");
    1346          20 :   keys.add("compulsory", "INTERVAL_MIN", "Lower limits of the domain of the basis functions. Outside the limits the bias is the one at the closest limit and the system does not feel the biasing force along that argument");
    1347          20 :   keys.add("compulsory", "INTERVAL_MAX", "Upper limits of the domain of the basis functions. Outside the limits the bias is the one at the closest limit and the system does not feel the biasing force along that argument");
    1348          20 :   keys.add("compulsory", "SKETCH_NBASIS", "20", "Number of basis functions per dimension");
    1349          20 :   keys.add("compulsory", "SKETCH_ALPHA", "0.05", "Weight coefficient for random tensor train construction");
    1350          20 :   keys.add("optional", "SKETCH_UNTIL", "After this time, the hills deposited so far are compressed into the TT one last time and the bias potential freezes");
    1351          20 :   keys.add("optional", "SKETCH_WIDTH", "Width of Gaussian kernels for smoothing");
    1352          20 :   keys.add("optional", "KERNEL_DX", "Width of basis function kernels");
    1353          20 :   keys.addFlag("NONINTRUSIVE", false, "Sketching uses previous exact sum of Gaussians instead of TT approximation");
    1354          20 :   keys.addFlag("DETERMINISTIC", false, "Use a fixed random seed for TT sketch construction, ensuring reproducible results. Intended for regression testing.");
    1355          20 : }
    1356             : 
    1357             : PLUMED_REGISTER_ACTION(TTMetaD, "TTMETAD")
    1358             : 
    1359             : } // namespace ttsketch
    1360             : } // namespace PLMD

Generated by: LCOV version 1.16