Source code for smt.surrogate_models.surrogate_model

"""
Author: Dr. Mohamed A. Bouhlel <mbouhlel@umich.edu>
        Dr. John T. Hwang <hwangjt@umich.edu>

This package is distributed under New BSD license.
Paul Saves : Mixed Integer
"""

from abc import ABCMeta, abstractmethod
from collections import defaultdict
from typing import Optional

import numpy as np

from smt.utils.checks import check_nx, check_support, ensure_2d_array
from smt.utils.options_dictionary import OptionsDictionary
from smt.utils.printer import Printer


[docs] class SurrogateModel(metaclass=ABCMeta): """ Base class for all surrogate models. Attributes ---------- options : OptionsDictionary Dictionary of options. Options values can be set on this attribute directly or they can be passed in as keyword arguments during instantiation. supports : dict Dictionary containing information about what this surrogate model supports. Examples -------- >>> from smt.surrogate_models import RBF >>> sm = RBF(print_training=False) >>> sm.options['print_prediction'] = False """
[docs] def __init__(self, **kwargs): """ Constructor where values of options can be passed in. For the list of options, see the documentation for the surrogate model being used. Parameters ---------- **kwargs : named arguments Set of options that can be optionally set; each option must have been declared. Examples -------- >>> from smt.surrogate_models import RBF >>> sm = RBF(print_global=False) """ self.options = OptionsDictionary() self.supports = supports = {} supports["training_derivatives"] = False supports["derivatives"] = False supports["output_derivatives"] = False supports["adjoint_api"] = False supports["variances"] = False supports["variance_derivatives"] = False supports["x_hierarchy"] = False declare = self.options.declare declare( "print_global", True, types=bool, desc="Global print toggle. If False, all printing is suppressed", ) declare( "print_training", True, types=bool, desc="Whether to print training information", ) declare( "print_prediction", True, types=bool, desc="Whether to print prediction information", ) declare( "print_problem", True, types=bool, desc="Whether to print problem information", ) declare( "print_solver", True, types=bool, desc="Whether to print solver information" ) self._initialize() self.options.update(kwargs) self.training_points = defaultdict(dict) self.printer = Printer() self._final_initialize()
@property @abstractmethod def name(self): pass
[docs] def set_training_values(self, xt: np.ndarray, yt: np.ndarray, name=None) -> None: """ Set training data (values). Parameters ---------- xt : np.ndarray[nt, nx] or np.ndarray[nt] The input values for the nt training points. yt : np.ndarray[nt, ny] or np.ndarray[nt] The output values for the nt training points. name : str or None An optional label for the group of training points being set. This is only used in special situations (e.g., multi-fidelity applications). """ xt = ensure_2d_array(xt, "xt") yt = ensure_2d_array(yt, "yt") if xt.shape[0] != yt.shape[0]: raise ValueError( "the first dimension of xt and yt must have the same length" ) self.nt = xt.shape[0] self.nx = xt.shape[1] self.ny = yt.shape[1] kx = 0 self.training_points[name][kx] = [np.array(xt), np.array(yt)]
def update_training_values( self, yt: np.ndarray, name: Optional[str] = None ) -> None: """ Update the training data (values) at the previously set input values. Parameters ---------- yt : np.ndarray[nt, ny] or np.ndarray[nt] The output values for the nt training points. name : str or None, optional An optional label for the group of training points being set. This is only used in special situations (e.g., multi-fidelity applications). The default is None. Raises ------ ValueError The training points must be set first with set_training_values before calling update_training_values. The number of training points does not agree with the earlier call of set_training_values. """ yt = ensure_2d_array(yt, "yt") kx = 0 if kx not in self.training_points[name]: raise ValueError( "The training points must be set first with set_training_values " + "before calling update_training_values." ) xt = self.training_points[name][kx][0] if xt.shape[0] != yt.shape[0]: raise ValueError( "The number of training points does not agree with the earlier call of " + "set_training_values." ) self.training_points[name][kx][1] = np.array(yt)
[docs] def set_training_derivatives( self, xt: np.ndarray, dyt_dxt: np.ndarray, kx: int, name: Optional[str] = None ) -> None: """ Set training data (derivatives). Parameters ---------- xt : np.ndarray[nt, nx] or np.ndarray[nt] The input values for the nt training points. dyt_dxt : np.ndarray[nt, ny] or np.ndarray[nt] The derivatives values for the nt training points. kx : int 0-based index of the derivatives being set. name : str or None An optional label for the group of training points being set. This is only used in special situations (e.g., multi-fidelity applications). """ check_support(self, "training_derivatives") xt = ensure_2d_array(xt, "xt") dyt_dxt = ensure_2d_array(dyt_dxt, "dyt_dxt") if xt.shape[0] != dyt_dxt.shape[0]: raise ValueError( "the first dimension of xt and dyt_dxt must have the same length" ) if not isinstance(kx, int): raise ValueError("kx must be an int") self.training_points[name][kx + 1] = [np.array(xt), np.array(dyt_dxt)]
def update_training_derivatives( self, dyt_dxt: np.ndarray, kx: int, name: Optional[str] = None ) -> None: """ Update the training data (values) at the previously set input values. Parameters ---------- dyt_dxt : np.ndarray[nt, ny] or np.ndarray[nt] The derivatives values for the nt training points. kx : int 0-based index of the derivatives being set. name :str or None, optional An optional label for the group of training points being set. This is only used in special situations (e.g., multi-fidelity applications). Raises ------ ValueError The training points must be set first with set_training_values before calling update_training_values.. The number of training points does not agree with the earlier call of set_training_values. """ check_support(self, "training_derivatives") dyt_dxt = ensure_2d_array(dyt_dxt, "dyt_dxt") if kx not in self.training_points[name]: raise ValueError( "The training points must be set first with set_training_values " + "before calling update_training_values." ) xt = self.training_points[name][kx][0] if xt.shape[0] != dyt_dxt.shape[0]: raise ValueError( "The number of training points does not agree with the earlier call of " + "set_training_values." ) self.training_points[name][kx + 1][1] = np.array(dyt_dxt)
[docs] def train(self) -> None: """ Train the model """ n_exact = self.training_points[None][0][0].shape[0] self.printer.active = self.options["print_global"] self.printer._line_break() self.printer._center(self.name) self.printer.active = ( self.options["print_global"] and self.options["print_problem"] ) self.printer._title("Problem size") self.printer(" %-25s : %i" % ("# training points.", n_exact)) self.printer() self.printer.active = ( self.options["print_global"] and self.options["print_training"] ) if self.name == "MixExp": # Mixture of experts model self.printer._title("Training of the Mixture of experts") else: self.printer._title("Training") # Train the model using the specified model-method with self.printer._timed_context("Training", "training"): self._train()
def _pre_predict(self, x): x = np.copy(x) x = ensure_2d_array(x, "x") self._check_xdim(x) n = x.shape[0] self.printer.active = ( self.options["print_global"] and self.options["print_prediction"] ) if self.name == "MixExp": # Mixture of experts model self.printer._title("Evaluation of the Mixture of experts") else: self.printer._title("Evaluation") self.printer(" %-12s : %i" % ("# eval points.", n)) self.printer() return x def _post_predict(self, x, y): n = x.shape[0] time_pt = self.printer._time("prediction")[-1] / n self.printer() self.printer("Prediction time/pt. (sec) : %10.7f" % time_pt) self.printer() return y
[docs] def predict_values(self, x: np.ndarray) -> np.ndarray: """ Predict the output values at a set of points. Parameters ---------- x : np.ndarray[nt, nx] or np.ndarray[nt] Input values for the prediction points. Returns ------- y : np.ndarray[nt, ny] Output values at the prediction points. """ x = self._pre_predict(x) # Evaluate the unknown points using the specified model-method with self.printer._timed_context("Predicting", key="prediction"): y = self._predict_values(x) return self._post_predict(x, y)
[docs] def predict_derivatives(self, x: np.ndarray, kx: int) -> np.ndarray: """ Predict the dy_dx derivatives at a set of points. Parameters ---------- x : np.ndarray[nt, nx] or np.ndarray[nt] Input values for the prediction points. kx : int The 0-based index of the input variable with respect to which derivatives are desired. Returns ------- dy_dx : np.ndarray[nt, ny] Derivatives. """ check_support(self, "derivatives") x = self._pre_predict(x) # Evaluate the unknown points using the specified model-method with self.printer._timed_context("Predicting", key="prediction"): y = self._predict_derivatives(x, kx) return self._post_predict(x, y)
[docs] def predict_output_derivatives(self, x: np.ndarray) -> dict: """ Predict the derivatives dy_dyt at a set of points. Parameters ---------- x : np.ndarray[nt, nx] or np.ndarray[nt] Input values for the prediction points. Returns ------- dy_dyt : dict of np.ndarray[nt, nt] Dictionary of output derivatives. Key is None for derivatives wrt yt and kx for derivatives wrt dyt_dxt. """ check_support(self, "output_derivatives") x = self._pre_predict(x) # Evaluate the unknown points using the specified model-method with self.printer._timed_context("Predicting", key="prediction"): y = self._predict_output_derivatives(x) return self._post_predict(x, y)
[docs] def predict_variances(self, x: np.ndarray) -> np.ndarray: """ Predict the variances at a set of points. Parameters ---------- x : np.ndarray[nt, nx] or np.ndarray[nt] Input values for the prediction points. Returns ------- s2 : np.ndarray[nt, ny] Variances. """ check_support(self, "variances") x = self._pre_predict(x) # Evaluate the unknown points using the specified model-method with self.printer._timed_context("Predicting", key="prediction"): y = self._predict_variances(x) return self._post_predict(x, y)
[docs] def predict_variance_derivatives(self, x: np.ndarray, kx: int) -> np.ndarray: """ Provide the derivatives of the variance of the model at a set of points Parameters ---------- x : np.ndarray [n_evals, dim] Evaluation point input variable values kx : int The 0-based index of the input variable with respect to which derivatives are desired. Returns ------- derived_variance: np.ndarray The kx-th derivatives of the variance of the kriging model """ check_support(self, "variance_derivatives") x = self._pre_predict(x) # Evaluate the unknown points using the specified model-method with self.printer._timed_context("Predicting", key="prediction"): y = self._predict_variance_derivatives(x, kx) return self._post_predict(x, y)
[docs] def predict_variance_gradient(self, x: np.ndarray) -> np.ndarray: """ Provide the gradient of the variance of the model at a given point (ie the derivatives wrt to all component at a unique point x) Parameters ---------- x : np.ndarray [1, dim] or even (dim,) vector Evaluation point input variable values Returns ------- derived_variance : np.ndarray The jacobian of the variance of the kriging model """ check_support(self, "variance_derivatives") if x.shape == (self.nx,): # allow to pass row vector for convenience x = np.atleast_2d(x.copy()) x = self._pre_predict(x) # Evaluate the unknown points using the specified model-method with self.printer._timed_context("Predicting", key="prediction"): y = self._predict_variance_gradient(x) return self._post_predict(x, y)
[docs] def _initialize(self): """ Implemented by surrogate models to declare options and declare what they support (optional). Examples -------- self.options.declare('option_name', default_value, types=(bool, int), desc='description') self.supports['derivatives'] = True """ pass
[docs] def _train(self) -> None: """ Implemented by surrogate models to perform training (optional, but typically implemented). """ pass
def _final_initialize(self): """ Implemented by surrogate models to complete the initialization after options are declared and possibly updated by the user. """ pass
[docs] @abstractmethod def _predict_values(self, x: np.ndarray) -> np.ndarray: """ Implemented by surrogate models to predict the output values. Parameters ---------- x : np.ndarray[nt, nx] Input values for the prediction points. Returns ------- y : np.ndarray[nt, ny] Output values at the prediction points. """ raise Exception("This surrogate model is incorrectly implemented")
[docs] def _predict_derivatives(self, x: np.ndarray, kx: int) -> np.ndarray: """ Implemented by surrogate models to predict the dy_dx derivatives (optional). If this method is implemented, the surrogate model should have `self.supports['derivatives'] = True` in the `_initialize()` implementation. Parameters ---------- x : np.ndarray[nt, nx] Input values for the prediction points. kx : int The 0-based index of the input variable with respect to which derivatives are desired. Returns ------- dy_dx : np.ndarray[nt, ny] Derivatives. """ check_support(self, "derivatives", fail=True)
[docs] def _predict_output_derivatives(self, x: np.ndarray) -> dict: """ Implemented by surrogate models to predict the dy_dyt derivatives (optional). If this method is implemented, the surrogate model should have `self.supports['derivatives'] = True` in the `_initialize()` implementation. Parameters ---------- x : np.ndarray[nt, nx] Input values for the prediction points. Returns ------- dy_dyt : dict of np.ndarray[nt, nt] Dictionary of output derivatives. Key is None for derivatives wrt yt and kx for derivatives wrt dyt_dxt. """ check_support(self, "output_derivatives", fail=True) return {}
[docs] def _predict_variances(self, x: np.ndarray) -> np.ndarray: """ Implemented by surrogate models to predict the variances at a set of points (optional). If this method is implemented, the surrogate model should have `self.supports['variances'] = True` in the `_initialize()` implementation. Parameters ---------- x : np.ndarray[nt, nx] Input values for the prediction points. is_acting : np.ndarray[nt, nx] or np.ndarray[nt] Matrix specifying for each design variable whether it is acting or not (for hierarchical design spaces) Returns ------- s2 : np.ndarray[nt, ny] Variances. """ check_support(self, "variances", fail=True)
def _predict_variance_derivatives(self, x: np.ndarray, kx: int): """ Implemented by surrogate models to predict the derivation of the variance at a point (optional). If this method is implemented, the surrogate model should have `self.supports['variance_derivatives'] = True` in the `_initialize()` implementation. """ check_support(self, "variance_derivatives", fail=True) def _predict_variance_gradient(self, x: np.ndarray): """ Implemented by surrogate models to predict the derivation of the variance at a point (optional). Parameters ----------- x : np.ndarray [1, dim] Evaluation point input variable values Returns ------- derived_variance: np.ndarray The gradient of the variance of the kriging model """ gradient = [ self._predict_variance_derivatives(self, x, kx) for kx in range(self.nx) ] return np.array(gradient) def _check_xdim(self, x): """Raise a ValueError if x dimension is not consistent with surrogate model training data dimension. This method is used as a guard in preamble of predict methods""" check_nx(self.nx, x) def save(self, filename): """ Implemented by surrogate models to save the surrogate object in a file """ raise NotImplementedError("save() has to be implemented by the given surrogate")