Creating a Surrogate Model

This example goes through the process of creating a custom surrogate model, in his case the creation of NearestPointSurrogate.

Overview

Building a surrogate model requires the creation of two objects: SurrogateTrainer and SurrogateModel. The SurrogateTrainer uses information from samplers and results to construct variables to be saved into a .rd file at the conclusion of the training run. The SurrogateModel object loads the data from the .rd and contains a function called evaluate that evaluates the surrogate model at a given input. The SurrogateTrainer and Surrogate are heavily tied together where each have the same member variables, the difference being one saves the data and the other loads it. It might be beneficial to have an interface class that contains common functions for training and evaluating, to avoid duplicate code. This example will not go into the creation of this interface class.

Creating a Trainer

This example will go over the creation of NearestPointTrainer. Trainers are derived from SurrogateTrainer which performs a loop over the training data and calls virtual functions that derived classes are meant to override to perform the proper training.

validParams

The trainer requires the input of a sampler, so that it understands how many data points are included and how they are distributed across processors. The trainer also needs the predictor and response values from the full-order model which are stored in a vector postprocessor or reporter.

InputParameters
SurrogateTrainer::validParams()
{
  InputParameters params = SurrogateTrainerBase::validParams();
  params.addRequiredParam<SamplerName>("sampler",
                                       "Sampler used to create predictor and response data.");
  params.addParam<ReporterName>(
      "converged_reporter",
      "Reporter value used to determine if a sample's multiapp solve converged.");
  params.addParam<bool>("skip_unconverged_samples",
                        false,
                        "True to skip samples where the multiapp did not converge, "
                        "'stochastic_reporter' is required to do this.");
  return params;
}
(modules/stochastic_tools/src/surrogates/SurrogateTrainer.C)
InputParameters
NearestPointTrainer::validParams()
{
  InputParameters params = SurrogateTrainer::validParams();
  params.addClassDescription("Loops over and saves sample values for [NearestPointSurrogate.md].");
  params.addRequiredParam<ReporterName>(
      "response",
      "Reporter value of response results, can be vpp with <vpp_name>/<vector_name> or sampler "
      "column with 'sampler/col_<index>'.");
  params.addParam<std::vector<ReporterName>>(
      "predictors",
      std::vector<ReporterName>(),
      "Reporter values used as the independent random variables, If 'predictors' and "
      "'predictor_cols' are both empty, all sampler columns are used.");
  params.addParam<std::vector<unsigned int>>(
      "predictor_cols",
      std::vector<unsigned int>(),
      "Sampler columns used as the independent random variables, If 'predictors' and "
      "'predictor_cols' are both empty, all sampler columns are used.");

  return params;
}
(modules/stochastic_tools/src/surrogates/NearestPointTrainer.C)

Constructor

All trainers are based on SurrogateTrainer, which provides the necessary interface for saving the surrogate model data and gathering response/predictor data. All the data meant to be saved and gathered is defined in the constructor of the training object. In NearestPointTrainer, the variable _sample_points is declared as the necessary surrogate data, see Trainers for more information on declaring model data. The variables _response, _predictors, and _predictor_cols refer to the data being used for training. _response and _predictors are in the form of reporter values and gathered through the getTrainingData API. _predictor_cols refer to the sampler column being used for training.

NearestPointTrainer::NearestPointTrainer(const InputParameters & parameters)
  : SurrogateTrainer(parameters),
    _sample_points(declareModelData<std::vector<std::vector<Real>>>("_sample_points")),
    _sampler_row(getSamplerData()),
    _response(getTrainingData<Real>(getParam<ReporterName>("response"))),
    _predictor_cols(getParam<std::vector<unsigned int>>("predictor_cols"))
{
  for (const ReporterName & rname : getParam<std::vector<ReporterName>>("predictors"))
    _predictors.push_back(&getTrainingData<Real>(rname));

  // If predictors and predictor_cols are empty, use all sampler columns
  if (_predictors.empty() && _predictor_cols.empty())
  {
    _predictor_cols.resize(_sampler.getNumberOfCols());
    std::iota(_predictor_cols.begin(), _predictor_cols.end(), 0);
  }

  // Resize sample points to number of predictors
  _sample_points.resize(_predictors.size() + _predictor_cols.size() + 1);
}
(modules/stochastic_tools/src/surrogates/NearestPointTrainer.C)

The member variables _sample_points, _response, _predictors, and _predictor_cols are defined in the header file:

  /// Map containing sample points and the results
  std::vector<std::vector<Real>> & _sample_points;

  /// Data from the current sampler row
  const std::vector<Real> & _sampler_row;

  /// Response value
  const Real & _response;

  /// Columns from sampler for predictors
  std::vector<unsigned int> _predictor_cols;

  /// Predictor values from reporters
  std::vector<const Real *> _predictors;
(modules/stochastic_tools/include/surrogates/NearestPointTrainer.h)

preTrain

preTrain() is called before the sampler loop. For NearestPointTrainer, we resize _sample_points appropriately:

void
NearestPointTrainer::preTrain()
{
  // Resize to number of sample points
  for (auto & it : _sample_points)
    it.resize(_sampler.getNumberOfLocalRows());
}
(modules/stochastic_tools/src/surrogates/NearestPointTrainer.C)

Note that getNumberOfLocalRows() is used to size the array, this is so that each processor contains a portion of the samples and results. We will gather all samples in postTrain().

train

train() is where the actual training occurs. This function is called during the sampler loop for each row, at which time the member variables _row, _local_row, and ones gathered with getTrainingData are updated:

void
NearestPointTrainer::train()
{
  unsigned int d = 0;
  // Get predictors from reporter values
  for (const auto & val : _predictors)
    _sample_points[d++][_local_row] = *val;
  // Get predictors from sampler
  for (const auto & col : _predictor_cols)
    _sample_points[d++][_local_row] = _sampler_row[col];

  _sample_points.back()[_local_row] = _response;
}
(modules/stochastic_tools/src/surrogates/NearestPointTrainer.C)

postTrain

postTrain() is called after the sampler loop. This is typically where processor communication happens. Here, we use postTrain() to gather all the local _sample_points so that each processor has the full copy. _communicator.allgather makes it so that every processor has a copy of the full array and _communicator.gather makes it so that only one of the processors has the full copy, the latter is typically used because outputting only happens on the root processor. See libMesh::Parallel::Communicator for more communication options.

void
NearestPointTrainer::postTrain()
{
  for (auto & it : _sample_points)
    _communicator.allgather(it);
}
(modules/stochastic_tools/src/surrogates/NearestPointTrainer.C)

Creating a Surrogate

This example will go over the creation of NearestPointSurrogate. Surrogates are a specialized version of a MooseObject that must have the evaluate public member function. The validParams for a surrogate will generally define how the surrogate is evaluated. NearestPointSurrogate does not have any options for the method of evaluation.

Constructor

In the constructor, the references for the model data are defined, taken from the training data:

NearestPointSurrogate::NearestPointSurrogate(const InputParameters & parameters)
  : SurrogateModel(parameters),
    _sample_points(getModelData<std::vector<std::vector<Real>>>("_sample_points"))
{
}
(modules/stochastic_tools/src/surrogates/NearestPointSurrogate.C)

See Surrogates for more information on the getModelData function. _sample_points in the surrogate is a const reference, since we do not want to modify the training data during evaluation:

  /// Array containing sample points and the results
  const std::vector<std::vector<Real>> & _sample_points;
(modules/stochastic_tools/include/surrogates/NearestPointSurrogate.h)

evaluate

evaluate is a public member function required for all surrogate models. This is where surrogate model is actually used. evaluate takes in parameter values and returns the surrogate's estimation of the quantity of interest. See EvaluateSurrogate for an example on how the evaluate function is used.

Real
NearestPointSurrogate::evaluate(const std::vector<Real> & x) const
{
  // Check whether input point has same dimensionality as training data
  mooseAssert((_sample_points.size() - 1) == x.size(),
              "Input point does not match dimensionality of training data.");

  // Returned value from training data (first sample is default)
  Real val = _sample_points.back()[0];

  // Container of current minimum distance during training sample loop
  Real dist_min = std::numeric_limits<Real>::max();

  for (dof_id_type p = 0; p < _sample_points[0].size(); ++p)
  {
    // Sum over the distance of each point dimension
    Real dist = 0;
    for (unsigned int i = 0; i < x.size(); ++i)
    {
      Real diff = (x[i] - _sample_points[i][p]);
      dist += diff * diff;
    }

    // Check if this training point distance is smaller than the current minimum
    if (dist < dist_min)
    {
      val = _sample_points.back()[p];
      dist_min = dist;
    }
  }

  return val;
}
(modules/stochastic_tools/src/surrogates/NearestPointSurrogate.C)