Source code for rocketpy.sensitivity.sensitivity_model

import numpy as np

from rocketpy.plots.sensitivity_plots import _SensitivityModelPlots
from rocketpy.prints.sensitivity_prints import _SensitivityModelPrints
from rocketpy.tools import check_requirement_version, import_optional_dependency


[docs] class SensitivityModel: """Performs a 'local variance based first-order sensitivity analysis' considering independent input parameters. The main reference for global variance based sensitivity analysis is [1]. Our method implements a local version that only considers first order terms, which correspond to linear terms. Albeit the flight function is nonlinear, the linear hypothesis might be adequate when performing *local* sensitivity analysis. The model is fit using separate multiple linear regression for each target variable passed and using the parameters as covariates. References ---------- [1] Sobol, Ilya M. "Global sensitivity indices for nonlinear mathematical models and their Monte Carlo estimates." Mathematics and computers in simulation 55.1-3 (2001): 271-280. """
[docs] def __init__( self, parameters_names, target_variables_names, ): """Initializes sensitivity model Parameters ---------- parameter_names: list[str] A list containing the names of the parameters used in the analysis. Note that the order is important and must match the order passed in the parameter data matrix. target_variables_names: list[str] A list containing the names of the target variables used in the analysis. Note that the order is important and must match the order passed in the target variables data matrix. Returns ------- None """ self.__check_requirements() self.n_parameters = len(parameters_names) self.parameters_names = parameters_names self.parameters_info = { parameter: { "nominal_mean": None, "nominal_sd": None, } for parameter in parameters_names } self.n_target_variables = len(target_variables_names) self.target_variables_names = target_variables_names self.target_variables_info = { variable: { "nominal_value": None, "sd": None, "var": None, "model": None, "sensitivity": {parameter: None for parameter in self.parameters_names}, "LAE": None, # Linear Approximation Error } for variable in target_variables_names } self.number_of_samples = None # Flags for nominal parameters self._nominal_parameters_passed = False self._nominal_target_passed = False self._fitted = False self.prints = _SensitivityModelPrints(self) self.plots = _SensitivityModelPlots(self)
[docs] def set_parameters_nominal(self, parameters_nominal_mean, parameters_nominal_sd): """Set parameters nominal mean and standard deviation Parameters ---------- parameters_nominal_mean : np.array An array containing the nominal mean for parameters in the order specified in parameters names at initialization parameters_nominal_sd : np.array An array containing the nominal standard deviation for parameters in the order specified in parameters names at initialization Returns ------- None """ if len(parameters_nominal_mean) != self.n_parameters: raise ValueError( "Nominal mean array length does not match number of " "parameters passed at initialization." ) if len(parameters_nominal_sd) != self.n_parameters: raise ValueError( "Nominal sd array length does not match number of parameters " "passed at initialization." ) for i, parameter in enumerate(self.parameters_names): self.parameters_info[parameter]["nominal_mean"] = parameters_nominal_mean[i] self.parameters_info[parameter]["nominal_sd"] = parameters_nominal_sd[i] self._nominal_parameters_passed = True
[docs] def set_target_variables_nominal(self, target_variables_nominal_value): """Set target variables nominal value (mean) Parameters ---------- target_variables_nominal_value: np.array An array containing the nominal mean for target variables in the order specified in target variables names at initialization Returns ------- None """ if len(target_variables_nominal_value) != self.n_target_variables: raise ValueError( "Target variables array length does not match number of \ target variables passed at initialization." ) for i, target_variable in enumerate(self.target_variables_names): self.target_variables_info[target_variable]["nominal_value"] = ( target_variables_nominal_value[i] ) self._nominal_target_passed = True
[docs] def _estimate_parameter_nominal(self, parameters_matrix): """Estimates parameters nominal values Parameters ---------- parameters_matrix : np.matrix Data matrix whose columns correspond to parameters values ordered as passed in initialization Returns ------- None """ for i, parameter in enumerate(self.parameters_names): self.parameters_info[parameter]["nominal_mean"] = np.mean( parameters_matrix[:, i] ) self.parameters_info[parameter]["nominal_sd"] = np.std( parameters_matrix[:, i] )
[docs] def _estimate_target_nominal(self, target_data): """Estimates target variables nominal values Parameters ---------- target_data : np.array | np.matrix Data matrix or array. In the case of a matrix, the columns correspond to target variable values ordered as passed in initialization Returns ------- None """ if target_data.ndim == 1: target_variable = self.target_variables_names[0] self.target_variables_info[target_variable]["nominal_value"] = np.mean( target_data[:] ) else: for i, target_variable in enumerate(self.target_variables_names): self.target_variables_info[target_variable]["nominal_value"] = np.mean( target_data[:, i] )
[docs] def fit(self, parameters_matrix, target_data): """Fits sensitivity model Parameters ---------- parameters_matrix : np.matrix Data matrix whose columns correspond to parameters values ordered as passed in initialization target_data : np.array | np.matrix Data matrix or array. In the case of a matrix, the columns correspond to target variable values ordered as passed in initialization Returns ------- None """ sm = import_optional_dependency("statsmodels.api") self.__check_conformity(parameters_matrix, target_data) # If nominal parameters are not set previous to fit, we must estimate them if not self._nominal_parameters_passed: self._estimate_parameter_nominal(parameters_matrix) if not self._nominal_target_passed: self._estimate_target_nominal(target_data) self.number_of_samples = parameters_matrix.shape[0] # Estimation setup parameters_mean = np.empty(self.n_parameters) parameters_sd = np.empty(self.n_parameters) for i, parameter in enumerate(self.parameters_names): parameters_mean[i] = self.parameters_info[parameter]["nominal_mean"] parameters_sd[i] = self.parameters_info[parameter]["nominal_sd"] offset_matrix = np.repeat(parameters_mean, self.number_of_samples) offset_matrix = offset_matrix.reshape( self.n_parameters, self.number_of_samples ).T X = parameters_matrix - offset_matrix X = sm.add_constant(X) # When target data is a 1d-array, transform to 2d-array if target_data.ndim == 1: target_data = target_data.reshape(self.number_of_samples, 1) self.__estimation_loop(target_data, sm, parameters_sd, X) self._fitted = True
def __estimation_loop(self, target_data, sm, parameters_sd, X): for i, target_variable in enumerate(self.target_variables_names): nominal_value = self.target_variables_info[target_variable]["nominal_value"] Y = np.array(target_data[:, i] - nominal_value) ols_model = sm.OLS(Y, X) fitted_model = ols_model.fit() self.target_variables_info[target_variable]["model"] = fitted_model # Compute sensitivity beta = fitted_model.params[1:] # skipping the intercept var_eps = fitted_model.scale var_y = var_eps for k, parameter in enumerate(self.parameters_names): sensitivity = np.power(beta[k], 2) * np.power(parameters_sd[k], 2) self.target_variables_info[target_variable]["sensitivity"][ parameter ] = sensitivity var_y += sensitivity self.target_variables_info[target_variable]["var"] = var_y self.target_variables_info[target_variable]["sd"] = np.sqrt(var_y) for k, parameter in enumerate(self.parameters_names): self.target_variables_info[target_variable]["sensitivity"][ parameter ] /= var_y self.target_variables_info[target_variable]["LAE"] = var_eps self.target_variables_info[target_variable]["LAE"] /= var_y def info(self): self.prints.all() def all_info(self): self.prints.all() self.plots.all() def __check_conformity( self, parameters_matrix, target_data, ): """Checks if matrices used for fitting conform with the information passed at initialization Parameters ---------- parameters_matrix : np.matrix Data matrix whose columns correspond to parameters values ordered as passed in initialization target_data : np.array | np.matrix Data matrix or array. In the case of a matrix, the columns correspond to target variable values ordered as passed in initialization Returns ------- None """ if parameters_matrix.shape[1] != self.n_parameters: raise ValueError( "Number of columns (parameters) does not match number of \ parameters passed at initialization." ) if target_data.ndim == 1: n_samples_y = len(target_data) if self.n_target_variables > 1: raise ValueError( "Single target variable passed but more than one target \ variable was passed at initialization." ) else: n_samples_y = target_data.shape[0] if target_data.shape[1] != self.n_target_variables: raise ValueError( "Number of columns (variables) does not match number of \ target variables passed at initialization." ) if n_samples_y != parameters_matrix.shape[0]: raise ValueError( "Number of samples does not match between parameter matrix \ and target data." )
[docs] def _raise_error_if_not_fitted(self): """Checks if model is fitted or raise an error. Returns ------- None """ if not self._fitted: raise ValueError("SensitivityModel must be fitted!")
def __check_requirements(self): """Check if extra requirements are installed. If not, print a message informing the user that some methods may not work and how to install the extra requirements. Returns ------- None """ sensitivity_require = { # The same as in the setup.py file "statsmodels": "", "prettytable": "", } has_error = False for module_name, version in sensitivity_require.items(): version = ">=0" if not version else version try: check_requirement_version(module_name, version) except (ValueError, ImportError) as e: # pragma: no cover has_error = True print( f"The following error occurred while importing {module_name}: {e}" ) if has_error: # pragma: no cover print( "Given the above errors, some methods may not work. Please run " + "'pip install rocketpy[sensitivity]' to install extra requirements." )