diff --git a/rocketpy/__init__.py b/rocketpy/__init__.py index 3b21cef83..aef87411d 100644 --- a/rocketpy/__init__.py +++ b/rocketpy/__init__.py @@ -37,6 +37,7 @@ Tail, TrapezoidalFins, ) +from .sensitivity import SensitivityModel from .simulation import Flight, MonteCarlo from .stochastic import ( StochasticEllipticalFins, @@ -49,5 +50,4 @@ StochasticTail, StochasticTrapezoidalFins, ) -from .sensitivity import SensitivityModel from .tools import load_monte_carlo_data diff --git a/rocketpy/sensitivity/sensivity_model.py b/rocketpy/sensitivity/sensivity_model.py index efb6855a5..5468cf7ce 100644 --- a/rocketpy/sensitivity/sensivity_model.py +++ b/rocketpy/sensitivity/sensivity_model.py @@ -1,6 +1,7 @@ -import numpy as np import matplotlib.pyplot as plt +import numpy as np from scipy.stats import norm + from ..tools import check_requirement_version, import_optional_dependency @@ -8,7 +9,7 @@ class SensitivityModel: """Performs a 'local variance based first-order sensitivity analysis' considering independent input parameters. - The core reference for global variance based sensitivity analysis is + 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 @@ -19,7 +20,9 @@ class SensitivityModel: 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. + [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. """ def __init__( @@ -27,6 +30,23 @@ def __init__( 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 @@ -72,24 +92,29 @@ def set_parameters_nominal( Parameters ---------- parameters_nominal_mean : np.array - An array contaning the nominal mean for parameters in the + An array containing the nominal mean for parameters in the order specified in parameters names at initialization parameters_nominal_sd : np.array - An array contaning the nominal standard deviation for + 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 initilization." + "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 initilization." + "Nominal sd array length does not match number of parameters \ + passed at initialization." ) - for i in range(self.n_parameters): - parameter = self.parameters_names[i] + 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] @@ -106,19 +131,23 @@ def set_target_variables_nominal( Parameters ---------- target_variables_nominal_value: np.array - An array contaning the nominal mean for target variables in + 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 initilization." - ) - for i in range(self.n_target_variables): - target_variable = self.target_variables_names[i] - self.target_variables_info[target_variable]["nominal_value"] = ( - target_variables_nominal_value[i] + "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 @@ -136,13 +165,12 @@ def _estimate_parameter_nominal( Data matrix whose columns correspond to parameters 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." - ) - for i in range(self.n_parameters): - parameter = self.parameters_names[i] + + for i, parameter in enumerate(self.parameters_names): self.parameters_info[parameter]["nominal_mean"] = np.mean( parameters_matrix[:, i] ) @@ -165,24 +193,18 @@ def _estimate_target_nominal( correspond to target variable values ordered as passed in initialization + Returns + ------- + None """ if target_data.ndim == 1: - if self.n_target_variables > 1: - raise ValueError( - "Single target variable passed but more than one target variable was passed at initialization." - ) target_variable = self.target_variables_names[0] self.target_variables_info[target_variable]["nominal_value"] = np.mean( target_data[:] ) else: - if target_data.shape[1] != self.n_target_variables: - raise ValueError( - "Number of columns (variables) does not match number of target variables passed at initilization." - ) - for i in range(self.n_target_variables): - target_variable = self.target_variables_names[i] + for i, target_variable in enumerate(self.target_variables_names): self.target_variables_info[target_variable]["nominal_value"] = np.mean( target_data[:, i] ) @@ -201,11 +223,14 @@ def fit( 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 """ # imports statsmodels for OLS method sm = import_optional_dependency("statsmodels.api") @@ -225,8 +250,7 @@ def fit( # Estimation setup parameters_mean = np.empty(self.n_parameters) parameters_sd = np.empty(self.n_parameters) - for i in range(self.n_parameters): - parameter = self.parameters_names[i] + 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"] @@ -241,8 +265,7 @@ def fit( target_data = target_data.reshape(self.number_of_samples, 1) # Estimation - for i in range(self.n_target_variables): - target_variable = self.target_variables_names[i] + 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) @@ -253,8 +276,7 @@ def fit( beta = fitted_model.params sd_eps = fitted_model.scale var_Y = sd_eps**2 - for k in range(self.n_parameters): - parameter = self.parameters_names[k] + 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 @@ -264,8 +286,7 @@ def fit( self.target_variables_info[target_variable]["var"] = var_Y self.target_variables_info[target_variable]["sd"] = np.sqrt(var_Y) - for k in range(self.n_parameters): - parameter = self.parameters_names[k] + for k, parameter in enumerate(self.parameters_names): self.target_variables_info[target_variable]["sensitivity"][ parameter ] /= var_Y @@ -276,13 +297,28 @@ def fit( return def plot(self, target_variable="all"): + """Creates barplot showing the sensitivity of the target_variable due + to parameters + + Parameters + ---------- + target_variable : str, optional + Name of the target variable used to show sensitivity. It can also + be "all", in which case a plot is created for each target variable + in which the model was fitted. The default is "all". + + Returns + ------- + None + """ self.__check_if_fitted() if (target_variable not in self.target_variables_names) and ( target_variable != "all" ): raise ValueError( - f"Target variable {target_variable} was not listed in initialization!" + f"Target variable {target_variable} was not listed in \ + initialization!" ) # Parameters bars are blue colored @@ -347,6 +383,10 @@ def summary(self, digits=4, alpha=0.95): Number of decimal digits printed on tables, by default 4 alpha: float, optional Significance level used for prediction intervals, by default 0.95 + + Returns + ------- + None """ self.__check_if_fitted() @@ -455,31 +495,44 @@ def __check_conformity( 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." + "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." + "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 initilization." + "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." + "Number of samples does not match between parameter matrix \ + and target data." ) return def __check_if_fitted(self): + """Checks if model is fitted + + Returns + ------- + None + """ if not self._fitted: raise Exception("SensitivityModel must be fitted!") return diff --git a/rocketpy/tools.py b/rocketpy/tools.py index e87125fd6..fd56ce1e5 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -9,6 +9,7 @@ import functools import importlib import importlib.metadata +import json import math import re import time @@ -19,7 +20,6 @@ from cftime import num2pydate from matplotlib.patches import Ellipse from packaging import version as packaging_version -import json # Mapping of module name and the name of the package that should be installed INSTALL_MAPPING = {"IPython": "ipython"} @@ -558,7 +558,7 @@ def load_monte_carlo_data( target_variables_list, ): """Reads MonteCarlo simulation data file and builds parameters and flight - variables matrices from specified + variables matrices Parameters ---------- @@ -566,34 +566,30 @@ def load_monte_carlo_data( Input file exported by MonteCarlo class. Each line is a sample unit described by a dictionary where keys are parameters names and the values are the sampled parameters values. - output_filename : str Output file exported by MonteCarlo.simulate function. Each line is a sample unit described by a dictionary where keys are target variables names and the values are the obtained values from the flight simulation. - parameters_list : list[str] List of parameters whose values will be extracted. - target_variables_list : list[str] List of target variables whose values will be extracted. Returns ------- parameters_matrix: np.matrix - Numpy matrix contaning input parameters values. Each column correspond + Numpy matrix containing input parameters values. Each column correspond to a parameter in the same order specified by 'parameters_list' input. - target_variables_matrix: np.matrix - Numpy matrix contaning target variables values. Each column correspond + Numpy matrix containing target variables values. Each column correspond to a target variable in the same order specified by 'target_variables_list' input. """ number_of_samples_parameters = 0 number_of_samples_variables = 0 - # Auxiliary function that unnests dictionary - def unnest_dict(x): + # Auxiliary function that flattens dictionary + def flatten_dict(x): new_dict = {} for key, value in x.items(): # the nested dictionary is inside a list @@ -601,7 +597,7 @@ def unnest_dict(x): # sometimes the object inside the list is another list # we must skip these cases if isinstance(value[0], dict): - inner_dict = unnest_dict(value[0]) + inner_dict = flatten_dict(value[0]) inner_dict = { key + "_" + inner_key: inner_value for inner_key, inner_value in inner_dict.items() @@ -618,14 +614,14 @@ def unnest_dict(x): number_of_samples_parameters += 1 parameters_dict = json.loads(line) - parameters_dict = unnest_dict(parameters_dict) + parameters_dict = flatten_dict(parameters_dict) for parameter in parameters_list: try: value = parameters_dict[parameter] - except Exception: - raise Exception( + except KeyError as e: + raise KeyError( f"Parameter {parameter} was not found in {input_filename}!" - ) + ) from e parameters_samples[parameter].append(value) target_variables_samples = {variable: [] for variable in target_variables_list} @@ -636,10 +632,10 @@ def unnest_dict(x): for variable in target_variables_list: try: value = target_variables_dict[variable] - except Exception: - raise Exception( + except KeyError as e: + raise KeyError( f"Variable {variable} was not found in {output_filename}!" - ) + ) from e target_variables_samples[variable].append(value) if number_of_samples_parameters != number_of_samples_variables: @@ -653,12 +649,10 @@ def unnest_dict(x): parameters_matrix = np.empty((n_samples, n_parameters)) target_variables_matrix = np.empty((n_samples, n_variables)) - for i in range(n_parameters): - parameter = parameters_list[i] + for i, parameter in enumerate(parameters_list): parameters_matrix[:, i] = parameters_samples[parameter] - for i in range(n_variables): - target_variable = target_variables_list[i] + for i, target_variable in enumerate(target_variables_list): target_variables_matrix[:, i] = target_variables_samples[target_variable] return parameters_matrix, target_variables_matrix diff --git a/setup.py b/setup.py deleted file mode 100644 index d0788eb9b..000000000 --- a/setup.py +++ /dev/null @@ -1,60 +0,0 @@ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -necessary_require = [ - "numpy>=1.13", - "scipy>=1.0", - "matplotlib>=3.0", - "netCDF4>=1.6.4", - "requests", - "pytz", - "simplekml", -] - -env_analysis_require = [ - "timezonefinder", - "windrose>=1.6.8", - "IPython", - "ipywidgets>=7.6.3", - "jsonpickle", -] - -monte_carlo_require = [ - "imageio", -] - -sensitivity_require = [ - "statsmodels", - "prettytable", -] - -setuptools.setup( - name="rocketpy", - version="1.2.1", - install_requires=necessary_require, - extras_require={ - "env_analysis": env_analysis_require, - "monte_carlo": monte_carlo_require, - "sensitivity": sensitivity_require, - "all": necessary_require - + env_analysis_require - + monte_carlo_require - + sensitivity_require, - }, - maintainer="RocketPy Developers", - author="Giovani Hidalgo Ceotto", - author_email="ghceotto@gmail.com", - description="Advanced 6-DOF trajectory simulation for High-Power Rocketry.", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/RocketPy-Team/RocketPy", - packages=setuptools.find_packages(), - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], - python_requires=">=3.8", -)