MUQ  0.4.3
SmolyakEstimator.cpp
Go to the documentation of this file.
2 
5 
6 #include <chrono>
7 
8 using namespace muq::Modeling;
9 using namespace muq::Utilities;
10 using namespace muq::Approximation;
11 
12 template<typename EstimateType>
13 SmolyakEstimator<EstimateType>::SmolyakEstimator(std::shared_ptr<muq::Modeling::ModPiece> const& modelIn)
14  : model(modelIn),
15  termMultis(std::make_shared<MultiIndexSet>(modelIn->inputSizes(0))),
16  pointCache(modelIn->inputSizes(0))
17 {
18  // make sure the model has a single input and output
19  assert(model->inputSizes.size()==1);
20  assert(model->outputSizes.size()==1);
21 }
22 
23 template<typename EstimateType>
24 EstimateType SmolyakEstimator<EstimateType>::ComputeWeightedSum(Eigen::VectorXd const& weights) const
25 {
26  assert(weights.size()<=terms.size());
27 
28  unsigned int firstNzInd = 0;
29  for(unsigned int i=0; i<weights.size(); ++i){
30  if(std::abs(weights(i))>nzTol) {
31  firstNzInd = i;
32  break;
33  }
34  }
35 
36  assert(std::abs(weights(firstNzInd))>nzTol);
37 
38  // compute the number of nonzero terms
39  EstimateType res = AddEstimates(0.0, terms.at(firstNzInd).val, weights(firstNzInd), terms.at(firstNzInd).val);
40  for(unsigned int i=firstNzInd+1; i<weights.size(); ++i){
41  if(std::abs(weights(i))>nzTol)
42  res = AddEstimates(1.0, res, weights(i), terms.at(i).val);
43  }
44 
45  return res;
46 }
47 
48 
49 template<typename EstimateType>
51 {
52  unsigned int firstNzInd = 0;
53  const double weightTol = 10.0*std::numeric_limits<double>::epsilon();
54  for(unsigned int i=0; i<terms.size(); ++i){
55  if(std::abs(terms.at(i).weight)>weightTol) {
56  firstNzInd = i;
57  break;
58  }
59  }
60 
61  // compute the number of nonzero terms
62  EstimateType res = AddEstimates(0.0, terms.at(firstNzInd).val, terms.at(firstNzInd).weight, terms.at(firstNzInd).val);
63  for(unsigned int i=firstNzInd+1; i<terms.size(); ++i){
64  if(std::abs(terms.at(i).weight)>weightTol){
65  res = AddEstimates(1.0, res, terms.at(i).weight, terms.at(i).val);
66  }
67  }
68 
69  return res;
70 }
71 
72 template<typename EstimateType>
73 EstimateType SmolyakEstimator<EstimateType>::Adapt(boost::property_tree::ptree options)
74 {
75  auto t_start = std::chrono::high_resolution_clock::now();
76  double runtime = 0.0; // <- Time since the beginning of adapting (in seconds)
77 
78  UpdateErrors();
79 
80  timeTol = options.get("MaximumAdaptTime", std::numeric_limits<double>::infinity());
81  errorTol = options.get("ErrorTol", 0.0);
82  maxNumEvals = options.get("MaximumEvals",std::numeric_limits<unsigned int>::max());
83 
84  // If none of the stopping criteria have been set, throw an error
85  if((timeTol==std::numeric_limits<double>::infinity())&&(errorTol==0)&&(maxNumEvals==std::numeric_limits<unsigned int>::max())){
86  std::stringstream msg;
87  msg << "Error in SmolyakEstimator::Adapt. No stopping criteria were set. ";
88  msg << "The options property_tree must specify at least one of the following ";
89  msg << "variables, \"MaximumAdaptTime\", \"ErrorTol\", or \"MaximumEvals\".";
90  throw std::runtime_error(msg.str());
91  }
92 
93  // Adapt until we've reached a stopping criteria
94  while((globalError>errorTol)&&(runtime<timeTol)&&(numEvals<maxNumEvals)) {
95  Refine();
96  UpdateErrors();
97 
98  auto t_curr =std::chrono::high_resolution_clock::now();
99  runtime = std::chrono::duration<double, std::milli>(t_curr-t_start).count()/1e3;
100 
101  // keep track of the convergence diagnostics
102  errorHistory.push_back(globalError);
103  timeHistory.push_back(runtime+timeHistory.at(0)); // Adding the zero is necessary because the first time corresponds to terms added before adaptation
104  }
105 
106 
107  return ComputeWeightedSum();
108 }
109 
110 template<typename EstimateType>
112 {
113  // Of all the terms on the frontier, find the one with the largest error indicator and expand it
114  int expandInd = -1;
115  double maxError = 0.0;
116  for(unsigned int termInd : termMultis->GetFrontier()){
117 
118  // Make sure all the backward neighbors are old
119  if(!terms.at(termInd).isOld){
120  if(terms.at(termInd).localError > maxError){
121  expandInd = termInd;
122  maxError = terms.at(termInd).localError;
123  }
124  }
125  }
126 
127  // If no admissible terms were found, just return
128  if(expandInd==-1)
129  return false;
130 
131  terms.at(expandInd).isOld = true;
132 
133  // Add the new terms to the set
134  std::vector<std::shared_ptr<MultiIndex>> neighs = termMultis->GetAdmissibleForwardNeighbors(expandInd);
135 
136  std::vector<std::shared_ptr<MultiIndex>> termsToAdd;
137  for(auto& neigh : neighs){
138 
139  // Make sure the forward neighbor is not already in the set and has old backward neighbors
140  if(!termMultis->IsActive(neigh)){
141 
142  bool shouldAdd = true;
143 
144  // Look through all of the backward neighbors. They should all be "old" to add this term.
145  std::vector<unsigned int> backNeighInds = termMultis->GetBackwardNeighbors(neigh);
146 
147  for(auto backInd : backNeighInds)
148  shouldAdd = shouldAdd && terms.at(backInd).isOld;
149 
150  if(shouldAdd)
151  termsToAdd.push_back(neigh);
152  }
153 
154 
155  }
156 
157  AddTerms(termsToAdd);
158 
159  return true;
160 }
161 
162 template<typename EstimateType>
163 void SmolyakEstimator<EstimateType>::AddTerms(std::shared_ptr<muq::Utilities::MultiIndexSet> const& fixedSet)
164 {
165  std::vector<std::shared_ptr<MultiIndex>> multiVec(fixedSet->Size());
166  for(unsigned int i=0; i<fixedSet->Size(); ++i)
167  multiVec.at(i) = fixedSet->IndexToMulti(i);
168 
169  AddTerms(multiVec);
170 }
171 
172 template<typename EstimateType>
173 std::vector<std::vector<Eigen::VectorXd>> SmolyakEstimator<EstimateType>::PointHistory() const
174 {
175  std::vector<std::vector<Eigen::VectorXd>> output(pointHistory.size());
176  for(int adaptInd=0; adaptInd<pointHistory.size(); ++adaptInd)
177  {
178  output.at(adaptInd).reserve(pointHistory.at(adaptInd).size());
179  for(auto& ptInd : pointHistory.at(adaptInd))
180  output.at(adaptInd).push_back( GetFromCache(ptInd) );
181  }
182  return output;
183 }
184 
185 
186 template<typename EstimateType>
187 void SmolyakEstimator<EstimateType>::AddTerms(std::vector<std::shared_ptr<MultiIndex>> const& fixedSet)
188 {
189  /* For each term in the fixed set we want to construct a tensor product approximation.
190  The i^th tensor product approximation will require model evaluations at N_i
191  points. The allNewPts vector will store all of the points that need to be evaluated
192  and stored in our cache. The evalInds vector will keep track of which points
193  were needed for each tensor product approximation by storing the index of
194  the cached points.
195  */
196 
197 
198  termHistory.push_back(fixedSet);
199 
200  for(unsigned int i=0; i<fixedSet.size(); ++i) {
201  unsigned int termInd = terms.size();
202  terms.push_back(SmolyTerm());
203  termMultis->AddActive(fixedSet.at(i));
204 
205  assert(terms.size()==termMultis->Size());
206 
207 
208  std::vector<Eigen::VectorXd> pts = OneTermPoints(fixedSet.at(i));
209 
210  // Figure out if we've already evaluated a point, or if we need to
211  terms.at(termInd).evalInds.resize(pts.size());
212  for(unsigned int k=0; k<pts.size(); ++k){
213 
214  // Check if the point is already in the cache
215  int cacheId = InCache(pts.at(k));
216  if(cacheId<0){
217  int cacheInd = AddToCache(pts.at(k));
218  terms.at(termInd).evalInds.at(k) = cacheInd;
219  evalCache.push_back( Eigen::VectorXd() );
220 
221  }else{
222  terms.at(termInd).evalInds.at(k) = cacheId;
223  }
224  }
225  }
226 
227  // Update the weights
228  Eigen::VectorXd newWeights = SmolyakQuadrature::ComputeWeights(termMultis);
229  for(unsigned int j=0; j<newWeights.size(); ++j)
230  terms.at(j).weight = newWeights(j);
231 
232  // Compute differential weights for the frontier terms
233  for(unsigned int termInd : termMultis->GetFrontier()){
234  terms.at(termInd).diffWeights = Eigen::VectorXd::Zero(terms.size());
235  SmolyakQuadrature::UpdateWeights(termInd, termMultis, terms.at(termInd).diffWeights);
236  }
237 
239  // Figure out which terms we need to actually compute.
240 
241  // All terms with nonzero Smolyak weights
242  for(unsigned int termInd=0; termInd<terms.size(); ++termInd){
243  if(std::abs(terms.at(termInd).weight) > nzTol)
244  terms.at(termInd).isNeeded = true;
245  }
246 
247  // All terms needed to compute error estimates on the "frontier" terms
248  for(unsigned int termInd : termMultis->GetFrontier()){
249  for(unsigned int wInd=0; wInd<terms.at(termInd).diffWeights.size(); ++wInd){
250  if(std::abs(terms.at(termInd).diffWeights(wInd))>nzTol)
251  terms.at(wInd).isNeeded = true;
252  }
253  }
254 
256  // Figure out what points we need to evaluate to build the terms we need
257  std::set<unsigned int> ptsToEval;
258  for(unsigned int termInd=0; termInd<terms.size(); ++termInd) {
259 
260  // If the smolyak weight is zero or the term has already been computed, don't bother computing any needed points
261  if((terms.at(termInd).isNeeded) && (!terms.at(termInd).isComputed)) {
262  for(unsigned int i=0; i<terms.at(termInd).evalInds.size(); ++i) {
263  unsigned int ptInd = terms.at(termInd).evalInds.at(i);
264  // If the size of the output is zero, we haven't evaluated the model at this point yet
265  if(evalCache.at(ptInd).size()==0)
266  ptsToEval.insert(ptInd);
267  }
268  }
269  }
270 
272  // Evaluate all the points we need to -- could easily be parallelized
273  EvaluatePoints(ptsToEval);
274 
275  pointHistory.push_back(ptsToEval);
276  evalHistory.push_back(numEvals);
277 
279  // Now, compute any new tensor product estimates that are necessary
280  for(unsigned int i=0; i<termMultis->Size(); ++i) {
281 
282  // If we haven't built this estimate before, do it now
283  if((terms.at(i).isNeeded) && (!terms.at(i).isComputed)) {
284 
285  // Copy references of the model output to a vector
286  std::vector<std::reference_wrapper<const Eigen::VectorXd>> evals;
287  for(unsigned int ptInd=0; ptInd<terms.at(i).evalInds.size(); ++ptInd){
288  evals.push_back( evalCache.at(terms.at(i).evalInds.at(ptInd) ) );
289  }
290  terms.at(i).val = ComputeOneTerm(termMultis->IndexToMulti(i), evals);
291  terms.at(i).isComputed = true;
292  }
293 
294  }
295 
296 }
297 
298 template<typename EstimateType>
299 void SmolyakEstimator<EstimateType>::EvaluatePoints(std::set<unsigned int> const& ptsToEval)
300 {
301  for(auto& ptInd : ptsToEval){
302  evalCache.at(ptInd) = model->Evaluate( GetFromCache(ptInd) ).at(0);
303  numEvals++;
304  }
305 }
306 
307 template<typename EstimateType>
309 {
310  globalError = 0.0;
311  // Consider terms that have non-active forward neighbors, which means it can be expandable
312  std::vector<unsigned int> frontierTerms = termMultis->GetFrontier();
313 
314  // For each frontier term, compute a local error estimate
315  for(unsigned int termInd : frontierTerms){
316  if(!terms.at(termInd).isOld){
317  terms.at(termInd).localError = ComputeMagnitude( ComputeWeightedSum( terms.at(termInd).diffWeights ));
318  globalError += terms.at(termInd).localError;
319  }
320  }
321 }
322 
323 template<typename EstimateType>
324 EstimateType SmolyakEstimator<EstimateType>::Compute(std::shared_ptr<muq::Utilities::MultiIndexSet> const& fixedSet,
325  boost::property_tree::ptree options)
326 {
327  auto t_start = std::chrono::high_resolution_clock::now();
328  double runtime = 0.0; // <- Time since the beginning of adapting (in seconds)
329 
330  Reset();
331 
332  AddTerms(fixedSet);
333 
334 
335  // Make all terms not on the strict frontier old
336  for(unsigned i=0;i<terms.size();++i)
337  terms.at(i).isOld = true;
338  for(unsigned int termInd : termMultis->GetStrictFrontier())
339  terms.at(termInd).isOld = false;
340 
341  UpdateErrors();
342 
343  // Keep track of how long that took us
344  auto t_curr =std::chrono::high_resolution_clock::now();
345  runtime = std::chrono::duration<double, std::milli>(t_curr-t_start).count()/1e3;
346  timeHistory.push_back(runtime);
347  errorHistory.push_back(globalError);
348 
349  bool shouldAdapt = options.get("ShouldAdapt",false);
350  if(shouldAdapt)
351  return Adapt(options);
352 
353  // We've done all the work, just return a weighted sum of the tensor product approximations
354  return ComputeWeightedSum();
355 }
356 
357 template<typename EstimateType>
359 {
360  termMultis = std::make_shared<MultiIndexSet>(model->inputSizes(0));
361  terms.clear();
362  globalError = std::numeric_limits<double>::infinity();
363 
364  errorHistory.clear();
365  evalHistory.clear();
366  timeHistory.clear();
367  termHistory.clear();
368  pointHistory.clear();
369  numEvals = 0;
370 }
371 
372 
373 // template<typename EstimateType>
374 // void SmolyakEstimator<EstimateType>::UpdateSmolyCoeffs(unsigned int const index)
375 // {
376 // // Get backward neighbors
377 // std::vector<unsigned int> neighInds = termMultis->GetBackwardNeighbors(index);
378 //
379 // Eigen::RowVectorXi baseMulti = termMultis->IndexToMulti(index)->GetVector();
380 // smolyWeights.at(index) += 1;
381 //
382 // for(unsigned int neighInd : neighInds) {
383 // int parity = (baseMulti - termMultis->IndexToMulti(neighInd)->GetVector()).sum();
384 // smolyWeights.at(neighInd) += ( (parity % 2 == 0) ? 1 : -1);
385 // }
386 // }
387 
388 template<typename EstimateType>
389 int SmolyakEstimator<EstimateType>::AddToCache(Eigen::VectorXd const& newPt)
390 {
391  int cacheId = InCache(newPt);
392  if(cacheId<0){
393  pointCache.add(newPt);
394  return CacheSize()-1;
395  }else{
396  return cacheId;
397  }
398 }
399 
400 template<typename EstimateType>
401 int SmolyakEstimator<EstimateType>::SmolyakEstimator::InCache(Eigen::VectorXd const& input) const
402 {
403  if(CacheSize()>0){
404  std::vector<size_t> indices;
405  std::vector<double> squaredDists;
406  std::tie(indices, squaredDists) = pointCache.query(input, 1);
407 
408  if(squaredDists.at(0)<cacheTol){
409  return indices.at(0);
410  }else{
411  return -1;
412  }
413 
414  }else{
415  return -1;
416  }
417 }
418 
419 namespace muq{
420 namespace Approximation{
421  template class SmolyakEstimator<Eigen::VectorXd>;
423 }
424 }
virtual std::vector< std::vector< Eigen::VectorXd > > PointHistory() const
int AddToCache(Eigen::VectorXd const &newPt)
virtual void EvaluatePoints(std::set< unsigned int > const &ptsToEval)
std::shared_ptr< muq::Modeling::ModPiece > model
The model used to construct the approximations.
virtual EstimateType Compute(std::shared_ptr< muq::Utilities::MultiIndexSet > const &fixedSet, boost::property_tree::ptree options=boost::property_tree::ptree())
virtual void AddTerms(std::shared_ptr< muq::Utilities::MultiIndexSet > const &fixedSet)
virtual EstimateType ComputeWeightedSum() const
virtual EstimateType Adapt(boost::property_tree::ptree options)
static void UpdateWeights(unsigned int activeInd, std::shared_ptr< muq::Utilities::MultiIndexSet > const &multis, Eigen::VectorXd &multiWeights)
static Eigen::VectorXd ComputeWeights(std::shared_ptr< muq::Utilities::MultiIndexSet > const &multis)
A class for holding, sorting, and adapting sets of multiindices.
Definition: MultiIndexSet.h:65