def __init__(self, \ t = ca.MX.sym("t", 1), \ u = ca.MX.sym("u", 0), \ p = None, \ phi = None, \ g = ca.MX.sym("g", 0)): intro.pecas_intro() print('\n' + 26 * '-' + \ ' PECas system definition ' + 27 * '-') print('\nStarting definition of BasicSystem system ...') if not all(isinstance(arg, ca.casadi.MX) for \ arg in [t, u, p, phi, g]): raise TypeError(''' Missing input argument for system definition or wrong variable type for an input argument. Input arguments must be CasADi symbolic types.''') self.t = t self.u = u self.p = p self.phi = phi self.g = g print('\nDefinition of BasicSystem system sucessful.')
def __init__(self): '''Placeholder-function for the according __init__()-methods of the classes that inherit from :class:`SetupsBaseClass`.''' intro.pecas_intro() print('\n' + 24 * '-' + \ ' PECas system initialization ' + 25 * '-') print('\nStart system initialization ...')
def __init__(self, \ t = ca.MX.sym("t", 1), u = ca.MX.sym("u", 0), \ x = None, \ p = None, \ eps_e = ca.MX.sym("eps_e", 0), \ eps_u = ca.MX.sym("eps_u", 0), \ phi = None, \ f = None): intro.pecas_intro() print('\n' + 26 * '-' + \ ' PECas system definition ' + 27 * '-') print('\nStarting definition of ExplODE system ...') if not all(isinstance(arg, ca.casadi.MX) for \ arg in [t, u, x, p, eps_e, eps_u, phi, f]): raise TypeError(''' Missing input argument for system definition or wrong variable type for an input argument. Input arguments must be CasADi symbolic types.''') if ca.dependsOn(f, t): raise NotImplementedError(''' Explicit time dependecies of the ODE right hand side are not yet supported in PECas, but probably will be in future versions.''') self.t = t self.u = u self.x = x self.eps_e = eps_e self.eps_u = eps_u self.p = p self.phi = phi self.f = f print('Definition of ExplODE system sucessful.')
def run_parameter_estimation(self, hessian="gauss-newton"): r''' :param hessian: Method of hessian calculation/approximation; possible values are `gauss-newton` and `exact-hessian` :type hessian: str This functions will run a least squares parameter estimation for the given problem and data set. For this, an NLP of the following structure is set up with a direct collocation approach and solved using IPOPT: .. math:: \begin{aligned} & \text{arg}\,\underset{x, p, v, \epsilon_e, \epsilon_u}{\text{min}} & & \frac{1}{2} \| R(w, v, \epsilon_e, \epsilon_u) \|_2^2 \\ & \text{subject to:} & & R(w, v, \epsilon_e, \epsilon_u) = w^{^\mathbb{1}/_\mathbb{2}} \begin{pmatrix} {v} \\ {\epsilon_e} \\ {\epsilon_u} \end{pmatrix} \\ & & & w = \begin{pmatrix} {w_{v}}^T & {w_{\epsilon_{e}}}^T & {w_{\epsilon_{u}}}^T \end{pmatrix} \\ & & & v_{l} + y_{l} - \phi(t_{l}, u_{l}, x_{l}, p) = 0 \\ & & & (t_{k+1} - t_{k}) f(t_{k,j}, u_{k,j}, x_{k,j}, p, \epsilon_{e,k,j}, \epsilon_{u,k,j}) - \sum_{r=0}^{d} \dot{L}_r(\tau_j) x_{k,r} = 0 \\ & & & x_{k+1,0} - \sum_{r=0}^{d} L_r(1) x_{k,r} = 0 \\ & & & t_{k,j} = t_k + (t_{k+1} - t_{k}) \tau_j \\ & & & L_r(\tau) = \prod_{r=0,r\neq j}^{d} \frac{\tau - \tau_r}{\tau_j - \tau_r}\\ & \text{for:} & & k = 1, \dots, N, ~~~ l = 1, \dots, M, ~~~ j = 1, \dots, d, ~~~ r = 1, \dots, d \\ & & & \tau_j = \text{time points w. r. t. scheme and order} \end{aligned} The status of IPOPT provides information whether the computation could be finished sucessfully. The optimal values for all optimization variables :math:`\hat{x}` can be accessed via the class variable ``LSq.Xhat``, while the estimated parameters :math:`\hat{p}` can also be accessed separately via the class attribute ``LSq.phat``. **Please be aware:** IPOPT finishing sucessfully does not necessarly mean that the estimation results for the unknown parameters are useful for your purposes, it just means that IPOPT was able to solve the given optimization problem. You have in any case to verify your results, e. g. by simulation using the class function :func:`run_simulation`. ''' intro.pecas_intro() print('\n' + 18 * '-' + \ ' PECas least squares parameter estimation ' + 18 * '-') print(''' Starting least squares parameter estimation using IPOPT, this might take some time ... ''') self.tstart_estimation = time.time() g = ca.vertcat([ca.vec(self.pesetup.phiN) - self.yN + \ ca.vec(self.pesetup.V)]) self.R = ca.sqrt(self.w) * \ ca.veccat([self.pesetup.V, self.pesetup.EPS_E, self.pesetup.EPS_U]) if self.pesetup.g.size(): g = ca.vertcat([g, self.pesetup.g]) self.g = g self.Vars = ca.veccat([ self.pesetup.P, \ self.pesetup.X, \ self.pesetup.XF, \ self.pesetup.V, \ self.pesetup.EPS_E, \ self.pesetup.EPS_U, \ ]) nlp = ca.MXFunction("nlp", ca.nlpIn(x=self.Vars), \ ca.nlpOut(f=(0.5 * ca.mul([self.R.T, self.R])), g=self.g)) options = {} options["tol"] = 1e-10 options["linear_solver"] = self.linear_solver if hessian == "gauss-newton": # ipdb.set_trace() gradF = nlp.gradient() jacG = nlp.jacobian("x", "g") # Can't the following be implemented more efficiently?! # gradF.derivative(0, 1) J = ca.jacobian(self.R, self.Vars) sigma = ca.MX.sym("sigma") hessLag = ca.MXFunction("H", \ ca.hessLagIn(x = self.Vars, lam_f = sigma), \ ca.hessLagOut(hess = sigma * ca.mul(J.T, J))) options["hess_lag"] = hessLag options["grad_f"] = gradF options["jac_g"] = jacG elif hessian == "exact-hessian": # let NlpSolver-class compute everything pass else: raise NotImplementedError( \ "Requested method is not implemented. Availabe methods " + \ "are 'gauss-newton' (default) and 'exact-hessian'.") # Initialize the solver, solve the optimization problem solver = ca.NlpSolver("solver", "ipopt", nlp, options) # Store the results of the computation Varsinit = ca.veccat([ self.pesetup.Pinit, \ self.pesetup.Xinit, \ self.pesetup.XFinit, \ self.pesetup.Vinit, \ self.pesetup.EPS_Einit, \ self.pesetup.EPS_Uinit, \ ]) sol = solver(x0=Varsinit, lbg=0, ubg=0) self.Varshat = sol["x"] R_squared_fcn = ca.MXFunction("R_squared_fcn", [self.Vars], [ca.mul([ \ ca.veccat([self.pesetup.V, self.pesetup.EPS_E, self.pesetup.EPS_U]).T, ca.veccat([self.pesetup.V, self.pesetup.EPS_E, self.pesetup.EPS_U])])]) [self.R_squared] = R_squared_fcn([self.Varshat]) self.tend_estimation = time.time() self.duration_estimation = self.tend_estimation - \ self.tstart_estimation print(''' Parameter estimation finished. Check IPOPT output for status information.''')
def show_system_information(self, showEquations = False): r''' :param showEquations: show model equations and measurement functions :type showEquations: bool This function shows the system type and the dimension of the system components. If `showEquations` is set to `True`, also the model equations and measurement functions are shown. ''' intro.pecas_intro() print('\n' + 26 * '-' + \ ' PECas system information ' + 26 * '-') if isinstance(self, BasicSystem): print('''\The system is a non-dynamic systems with the general input-output structure and contrain equations: ''') print("y = phi(t, u, p), g(t, u, p) = 0 ") print('''\nWith {0} inputs u, {1} parameters p and {2} outputs phi '''.format(self.u.size(),self.p.size(), \ self.phi.size())) if showEquations: print("\nAnd where phi is defined by: ") for i, yi in enumerate(self.phi): print("y[{0}] = {1}".format(\ i, yi)) print("\nAnd where g is defined by: ") for i, gi in enumerate(self.g): print("g[{0}] = {1}".format(\ i, gi)) elif isinstance(self, ExplODE): print('''\nThe system is a dynamic system defined by a set of explicit ODEs xdot which establish the system state x: xdot = f(t, u, x, p, eps_e, eps_u) and by an output function phi which sets the system measurements: y = phi(t, x, p). ''') print('''Particularly, the system has: {0} inputs u {1} parameters p {2} states x {3} outputs phi'''.format(self.u.size(),self.p.size(),\ self.x.size(), \ self.phi.size())) if showEquations: print("\nWhere xdot is defined by: ") for i, xi in enumerate(self.f): print("xdot[{0}] = {1}".format(\ i, xi)) print("\nAnd where phi is defined by: ") for i, yi in enumerate(self.phi): print("y[{0}] = {1}".format(\ i, yi)) else: raise NotImplementedError(''' This feature of PECas is currently disabled, but will be available when the DAE systems are implemented. ''')
def show_system_information(self, showEquations=False): r''' :param showEquations: show model equations and measurement functions :type showEquations: bool This function shows the system type and the dimension of the system components. If `showEquations` is set to `True`, also the model equations and measurement functions are shown. ''' intro.pecas_intro() print('\n' + 26 * '-' + \ ' PECas system information ' + 26 * '-') if isinstance(self, BasicSystem): print('''\The system is a non-dynamic systems with the general input-output structure and contrain equations: ''') print("y = phi(t, u, p), g(t, u, p) = 0 ") print('''\nWith {0} inputs u, {1} parameters p and {2} outputs phi '''.format(self.u.size(),self.p.size(), \ self.phi.size())) if showEquations: print("\nAnd where phi is defined by: ") for i, yi in enumerate(self.phi): print("y[{0}] = {1}".format(\ i, yi)) print("\nAnd where g is defined by: ") for i, gi in enumerate(self.g): print("g[{0}] = {1}".format(\ i, gi)) elif isinstance(self, ExplODE): print('''\nThe system is a dynamic system defined by a set of explicit ODEs xdot which establish the system state x: xdot = f(t, u, x, p, eps_e, eps_u) and by an output function phi which sets the system measurements: y = phi(t, x, p). ''') print('''Particularly, the system has: {0} inputs u {1} parameters p {2} states x {3} outputs phi'''.format(self.u.size(),self.p.size(),\ self.x.size(), \ self.phi.size())) if showEquations: print("\nWhere xdot is defined by: ") for i, xi in enumerate(self.f): print("xdot[{0}] = {1}".format(\ i, xi)) print("\nAnd where phi is defined by: ") for i, yi in enumerate(self.phi): print("y[{0}] = {1}".format(\ i, yi)) else: raise NotImplementedError(''' This feature of PECas is currently disabled, but will be available when the DAE systems are implemented. ''')
def show_results(self): r''' :raises: AttributeError This function displays the results of the parameter estimation computations. It can not be used before function :func:`run_parameter_estimation()` has been used. The results displayed by the function contain: - the values of the estimated parameters :math:`\hat{p}` and their corresponding standard deviations (the values of the standard deviations are presented only if the covariance matrix had already been computed), - the values of the covariance matrix :math:`\Sigma_{\hat{p}}` for the estimated parameters (if it had already been computed), - in the case of the estimation of a dynamic system, the estimated value of the first state :math:`\hat{x}(t_{0})` and the estimated value of the last state :math:`\hat{x}(t_{N})`, - the value of :math:`R^2` measuring the goodness of fit of the estimated parameters, and - the durations of the setup and the estimation. ''' intro.pecas_intro() np.set_printoptions(linewidth = 200, \ formatter={'float': lambda x: format(x, ' 10.8e')}) try: print('\n' + 21 * '-' + \ ' PECas Parameter estimation results ' + 21 * '-') print("\nEstimated parameters p_i:") for i, xi in enumerate(self.phat): try: print(" p_{0:<3} = {1: 10.8e} +/- {2: 10.8e}".format(\ i, xi[0], ca.sqrt(abs(self.Covp[i, i])))) except AttributeError: print(" p_{0:<3} = {1: 10.8e}".format(\ i, xi[0])) try: print("\nInitial states value estimated from collocation: ") print(" x(t_0) = {0}".format(self.Xhat[:, 0])) print("\nFinal states value estimated from collocation: ") print(" x(t_N) = {0}".format(self.Xhat[:, -1])) except AttributeError: pass print("\nCovariance matrix for the estimated parameters:") try: print(np.atleast_2d(self.Covp)) except AttributeError: print( \ ''' Covariance matrix for the estimated parameters not yet computed. Run class function compute_covariance_matrix() to do so.''') print( \ "\nGoodness of fit R^2" + 30 * "." + ": {0:10.8e}".format(\ float(self.R_squared))) print("\nDuration of the problem setup"+ 20 * "." + \ ": {0:10.8e} s".format(self.pesetup.duration_setup)) print("Duration of the estimation" + 23 * "." + \ ": {0:10.8e} s".format(self.duration_estimation)) try: print("Duration of the covariance matrix computation...." + \ ": {0:10.8e} s".format(self.duration_cov_computation)) except AttributeError: pass except AttributeError: raise AttributeError(''' You must execute at least run_parameter_estimation() to obtain results, and compute_covariance_matrix() before all results can be displayed. ''') finally: np.set_printoptions()
def compute_covariance_matrix(self): r''' This function computes the covariance matrix of the estimated parameters from the inverse of the KKT matrix for the parameter estimation problem. This allows then for statements on the quality of the values of the estimated parameters. For efficiency, only the inverse of the relevant part of the matrix is computed using the Schur complement. A more detailed description of this function will follow in future versions. ''' intro.pecas_intro() print('\n' + 20 * '-' + \ ' PECas covariance matrix computation ' + 21 * '-') print(''' Computing the covariance matrix for the estimated parameters, this might take some time ... ''') self.tstart_cov_computation = time.time() try: N1 = ca.MX(self.Vars.shape[0] - self.w.shape[0], \ self.w.shape[0]) N2 = ca.MX(self.Vars.shape[0] - self.w.shape[0], \ self.Vars.shape[0] - self.w.shape[0]) hess = ca.blockcat([ [N2, N1], [N1.T, ca.diag(self.w)], ]) # hess = hess + 1e-10 * ca.diag(self.Vars) # J2 can be re-used from parameter estimation, right? J2 = ca.jacobian(self.g, self.Vars) kkt = ca.blockcat( \ [[hess, \ J2.T], \ [J2, \ ca.MX(self.g.size1(), self.g.size1())]] \ ) B1 = kkt[:self.pesetup.np, :self.pesetup.np] E = kkt[self.pesetup.np:, :self.pesetup.np] D = kkt[self.pesetup.np:, self.pesetup.np:] Dinv = ca.solve(D, E, "csparse") F11 = B1 - ca.mul([E.T, Dinv]) self.fbeta = ca.MXFunction("fbeta", [self.Vars], [ca.mul([self.R.T, self.R]) / \ (self.yN.size + self.g.size1() - self.Vars.size())]) [self.beta] = self.fbeta([self.Varshat]) self.fcovp = ca.MXFunction("fcovp", [self.Vars], \ [self.beta * ca.solve(F11, ca.MX.eye(F11.size1()))]) [self.Covp] = self.fcovp([self.Varshat]) print( \ '''Covariance matrix computation finished, run show_results() to visualize.''') except AttributeError as err: errmsg = ''' You must execute run_parameter_estimation() first before the covariance matrix for the estimated parameters can be computed. ''' raise AttributeError(errmsg) finally: self.tend_cov_computation = time.time() self.duration_cov_computation = self.tend_cov_computation - \ self.tstart_cov_computation
def run_simulation(self, \ x0 = None, tsim = None, usim = None, psim = None, method = "rk"): r''' :param x0: initial value for the states :math:`x_0 \in \mathbb{R}^{n_x}` :type x0: list, numpy,ndarray, casadi.DMatrix :param tsim: optional, switching time points for the controls :math:`t_{sim} \in \mathbb{R}^{L}` to be used for the simulation :type tsim: list, numpy,ndarray, casadi.DMatrix :param usim: optional, control values :math:`u_{sim} \in \mathbb{R}^{n_u \times L}` to be used for the simulation :type usim: list, numpy,ndarray, casadi.DMatrix :param psim: optional, parameter set :math:`p_{sim} \in \mathbb{R}^{n_p}` to be used for the simulation :type psim: list, numpy,ndarray, casadi.DMatrix :param method: optional, CasADi integrator to be used for the simulation :type method: str This function performs a simulation of the system for a given parameter set :math:`p_{sim}`, starting from a user-provided initial value for the states :math:`x_0`. If the argument ``psim`` is not specified, the estimated parameter set :math:`\hat{p}` is used. For this, a parameter estimation using :func:`run_parameter_estimation()` has to be done beforehand, of course. By default, the switching time points for the controls :math:`t_u` and the corresponding controls :math:`u_N` will be used for simulation. If desired, other time points :math:`t_{sim}` and corresponding controls :math:`u_{sim}` can be passed to the function. For the moment, the function can only be used for systems of type :class:`pecas.systems.ExplODE`. ''' intro.pecas_intro() print('\n' + 27 * '-' + \ ' PECas system simulation ' + 26 * '-') print('\nPerforming system simulation, this might take some time ...') if not type(self.pesetup.system) is systems.ExplODE: raise NotImplementedError("Until now, this function can only " + \ "be used for systems of type ExplODE.") if x0 == None: raise ValueError("You have to provide an initial value x0 " + \ "to run the simulation.") x0 = np.squeeze(np.asarray(x0)) if np.atleast_1d(x0).shape[0] != self.pesetup.nx: raise ValueError("Wrong dimension for initial value x0.") if tsim == None: tsim = self.pesetup.tu if usim == None: usim = self.pesetup.uN if psim == None: try: psim = self.phat except AttributeError: errmsg = ''' You have to either perform a parameter estimation beforehand to obtain a parameter set that can be used for simulation, or you have to provide a parameter set in the argument psim. ''' raise AttributeError(errmsg) else: if not np.atleast_1d(np.squeeze(psim)).shape[0] == self.pesetup.np: raise ValueError("Wrong dimension for parameter set psim.") fp = ca.MXFunction("fp", \ [self.pesetup.system.t, self.pesetup.system.u, \ self.pesetup.system.x, self.pesetup.system.eps_e, \ self.pesetup.system.eps_u, self.pesetup.system.p], \ [self.pesetup.system.f]) fpeval = fp([\ self.pesetup.system.t, self.pesetup.system.u, \ self.pesetup.system.x, np.zeros(self.pesetup.neps_e), \ np.zeros(self.pesetup.neps_u), psim])[0] fsim = ca.MXFunction("fsim", \ ca.daeIn(t = self.pesetup.system.t, \ x = self.pesetup.system.x, \ p = self.pesetup.system.u), \ ca.daeOut(ode = fpeval)) Xsim = [] Xsim.append(x0) u0 = ca.DMatrix() for k, e in enumerate(tsim[:-1]): try: integrator = ca.Integrator("integrator", method, \ fsim, {"t0": e, "tf": tsim[k+1]}) except RuntimeError as err: errmsg = ''' It seems like you want to use an integration method that is not currently supported by CasADi. Please refer to the CasADi documentation for a list of supported integrators, or use the default RK4-method by not setting the method-argument of the function. ''' raise RuntimeError(errmsg) if not self.pesetup.nu == 0: u0 = usim[:, k] Xk_end = itemgetter('xf')(integrator({'x0': x0, 'p': u0})) Xsim.append(Xk_end) x0 = Xk_end self.Xsim = ca.horzcat(Xsim) print( \ '''System simulation finished.''')
def __init__(self, system = None, \ tu = None, uN = None, \ ty = None, yN = None, \ wv = None, weps_e = None, weps_u = None, \ pinit = None, \ xinit = None, \ linear_solver = None, \ scheme = None, \ order = None): intro.pecas_intro() print('\n' + 22 * '-' + \ ' PECas parameter estimation setup ' + 22 * '-') print('\nStarting parameter estimation problem setup ...') self.linear_solver = linear_solver if type(system) is systems.BasicSystem: self.pesetup = setups.BSsetup(system = system, \ tu = tu, uN = uN, \ pinit = pinit) elif type(system) is systems.ExplODE: self.pesetup = setups.ODEsetup(system = system, \ tu = tu, uN = uN, \ ty = ty, yN = yN, \ pinit = pinit, \ xinit = xinit, \ scheme = scheme, \ order = order) else: raise NotImplementedError( \ "The system type provided by the user is not supported.") # Store the parameter estimation problem setup # self.pesetup = pesetup # Check if the supported measurement data fits to the dimensions of # the output function yN = np.atleast_2d(yN) if yN.shape == (self.pesetup.tu.size, self.pesetup.nphi): yN = yN.T if not yN.shape == (self.pesetup.nphi, self.pesetup.tu.size): raise ValueError(''' The dimension of the measurement data given in yN does not match the dimension of output function and/or tu. Valid dimensions for yN for the given data are: {0} or {1}, but you supported yN of dimension: {2}.'''.format(str((self.pesetup.tu.size, self.pesetup.nphi)), \ str((self.pesetup.nphi, self.pesetup.tu.size)), str(yN.shape))) # Check if the supported standard deviations fit to the dimensions of # the measurement data wv = np.atleast_2d(wv) if wv.shape == yN.T.shape: wv = wv.T if not wv.shape == yN.shape: raise ValueError(''' The dimension of weights of the measurement errors given in wv does not match the dimensions of the measurement data. Valid dimensions for wv for the given data are: {0} or {1}, but you supported wv of dimension: {2}.'''.format(str(yN.shape), str(yN.T.shape), str(wv.shape))) # Get the measurement values and standard deviations into the # necessary order of apperance and dimensions self.yN = np.zeros(np.size(yN)) self.wv = np.zeros(np.size(yN)) for k in range(yN.shape[0]): self.yN[k:yN.shape[0]*yN.shape[1]+1:yN.shape[0]] = \ yN[k, :] self.wv[k:yN.shape[0]*yN.shape[1]+1:yN.shape[0]] = \ wv[k, :] self.weps_e = [] try: if self.pesetup.neps_e != 0: weps_e = np.atleast_2d(weps_e) try: if weps_e.shape == (1, self.pesetup.neps_e): weps_e = weps_e.T if not weps_e.shape == (self.pesetup.neps_e, 1): raise ValueError(''' The dimensions of the weights of the equation errors given in weps_e does not match the dimensions of the equation errors given in eps_e.''') self.weps_e = weps_e except AttributeError: pass try: self.weps_e = np.squeeze(ca.repmat(weps_e, self.pesetup.nsteps * \ (len(self.pesetup.tauroot)-1), 1)) except AttributeError: self.weps_e = [] except AttributeError: pass self.weps_u = [] try: if self.pesetup.neps_u != 0: weps_u = np.atleast_2d(weps_u) try: if weps_u.shape == (1, self.pesetup.neps_u): weps_u = weps_u.T if not weps_u.shape == (self.pesetup.neps_u, 1): raise ValueError(''' The dimensions of the weights of the input errors given in weps_u does not match the dimensions of the input errors given in eps_u.''') self.weps_u = weps_u except AttributeError: pass try: self.weps_u = np.squeeze(ca.repmat(weps_u, self.pesetup.nsteps * \ (len(self.pesetup.tauroot)-1), 1)) except AttributeError: self.weps_u = [] except AttributeError: pass # Set up the covariance matrix for the measurements # self.w = ca.diag(np.concatenate((self.wv, self.weps_e,self.weps_u))) self.w = ca.veccat((self.wv, self.weps_e, self.weps_u)) print('Setup of the parameter estimation problem sucessful.')
def show_results(self): r''' :raises: AttributeError This function displays the results of the parameter estimation computations. It can not be used before function :func:`run_parameter_estimation()` has been used. The results displayed by the function contain: - the values of the estimated parameters :math:`\hat{p}` and their corresponding standard deviations (the values of the standard deviations are presented only if the covariance matrix had already been computed), - the values of the covariance matrix :math:`\Sigma_{\hat{p}}` for the estimated parameters (if it had already been computed), - in the case of the estimation of a dynamic system, the estimated value of the first state :math:`\hat{x}(t_{0})` and the estimated value of the last state :math:`\hat{x}(t_{N})`, - the value of :math:`R^2` measuring the goodness of fit of the estimated parameters, and - the durations of the setup and the estimation. ''' intro.pecas_intro() np.set_printoptions(linewidth = 200, \ formatter={'float': lambda x: format(x, ' 10.8e')}) try: print('\n' + 21 * '-' + \ ' PECas Parameter estimation results ' + 21 * '-') print("\nEstimated parameters p_i:") for i, xi in enumerate(self.phat): try: print(" p_{0:<3} = {1: 10.8e} +/- {2: 10.8e}".format(\ i, xi[0], ca.sqrt(abs(self.Covp[i, i])))) except AttributeError: print(" p_{0:<3} = {1: 10.8e}".format(\ i, xi[0])) try: print("\nInitial states value estimated from collocation: ") print(" x(t_0) = {0}".format(self.Xhat[:,0])) print("\nFinal states value estimated from collocation: ") print(" x(t_N) = {0}".format(self.Xhat[:,-1])) except AttributeError: pass print("\nCovariance matrix for the estimated parameters:") try: print(np.atleast_2d(self.Covp)) except AttributeError: print( \ ''' Covariance matrix for the estimated parameters not yet computed. Run class function compute_covariance_matrix() to do so.''') print( \ "\nGoodness of fit R^2" + 30 * "." + ": {0:10.8e}".format(\ float(self.R_squared))) print("\nDuration of the problem setup"+ 20 * "." + \ ": {0:10.8e} s".format(self.pesetup.duration_setup)) print("Duration of the estimation" + 23 * "." + \ ": {0:10.8e} s".format(self.duration_estimation)) try: print("Duration of the covariance matrix computation...." + \ ": {0:10.8e} s".format(self.duration_cov_computation)) except AttributeError: pass except AttributeError: raise AttributeError(''' You must execute at least run_parameter_estimation() to obtain results, and compute_covariance_matrix() before all results can be displayed. ''') finally: np.set_printoptions()
def compute_covariance_matrix(self): r''' This function computes the covariance matrix of the estimated parameters from the inverse of the KKT matrix for the parameter estimation problem. This allows then for statements on the quality of the values of the estimated parameters. For efficiency, only the inverse of the relevant part of the matrix is computed using the Schur complement. A more detailed description of this function will follow in future versions. ''' intro.pecas_intro() print('\n' + 20 * '-' + \ ' PECas covariance matrix computation ' + 21 * '-') print(''' Computing the covariance matrix for the estimated parameters, this might take some time ... ''') self.tstart_cov_computation = time.time() try: N1 = ca.MX(self.Vars.shape[0] - self.w.shape[0], \ self.w.shape[0]) N2 = ca.MX(self.Vars.shape[0] - self.w.shape[0], \ self.Vars.shape[0] - self.w.shape[0]) hess = ca.blockcat([[N2, N1], [N1.T, ca.diag(self.w)],]) # hess = hess + 1e-10 * ca.diag(self.Vars) # J2 can be re-used from parameter estimation, right? J2 = ca.jacobian(self.g, self.Vars) kkt = ca.blockcat( \ [[hess, \ J2.T], \ [J2, \ ca.MX(self.g.size1(), self.g.size1())]] \ ) B1 = kkt[:self.pesetup.np, :self.pesetup.np] E = kkt[self.pesetup.np:, :self.pesetup.np] D = kkt[self.pesetup.np:, self.pesetup.np:] Dinv = ca.solve(D, E, "csparse") F11 = B1 - ca.mul([E.T, Dinv]) self.fbeta = ca.MXFunction("fbeta", [self.Vars], [ca.mul([self.R.T, self.R]) / \ (self.yN.size + self.g.size1() - self.Vars.size())]) [self.beta] = self.fbeta([self.Varshat]) self.fcovp = ca.MXFunction("fcovp", [self.Vars], \ [self.beta * ca.solve(F11, ca.MX.eye(F11.size1()))]) [self.Covp] = self.fcovp([self.Varshat]) print( \ '''Covariance matrix computation finished, run show_results() to visualize.''') except AttributeError as err: errmsg = ''' You must execute run_parameter_estimation() first before the covariance matrix for the estimated parameters can be computed. ''' raise AttributeError(errmsg) finally: self.tend_cov_computation = time.time() self.duration_cov_computation = self.tend_cov_computation - \ self.tstart_cov_computation
def run_simulation(self, \ x0 = None, tsim = None, usim = None, psim = None, method = "rk"): r''' :param x0: initial value for the states :math:`x_0 \in \mathbb{R}^{n_x}` :type x0: list, numpy,ndarray, casadi.DMatrix :param tsim: optional, switching time points for the controls :math:`t_{sim} \in \mathbb{R}^{L}` to be used for the simulation :type tsim: list, numpy,ndarray, casadi.DMatrix :param usim: optional, control values :math:`u_{sim} \in \mathbb{R}^{n_u \times L}` to be used for the simulation :type usim: list, numpy,ndarray, casadi.DMatrix :param psim: optional, parameter set :math:`p_{sim} \in \mathbb{R}^{n_p}` to be used for the simulation :type psim: list, numpy,ndarray, casadi.DMatrix :param method: optional, CasADi integrator to be used for the simulation :type method: str This function performs a simulation of the system for a given parameter set :math:`p_{sim}`, starting from a user-provided initial value for the states :math:`x_0`. If the argument ``psim`` is not specified, the estimated parameter set :math:`\hat{p}` is used. For this, a parameter estimation using :func:`run_parameter_estimation()` has to be done beforehand, of course. By default, the switching time points for the controls :math:`t_u` and the corresponding controls :math:`u_N` will be used for simulation. If desired, other time points :math:`t_{sim}` and corresponding controls :math:`u_{sim}` can be passed to the function. For the moment, the function can only be used for systems of type :class:`pecas.systems.ExplODE`. ''' intro.pecas_intro() print('\n' + 27 * '-' + \ ' PECas system simulation ' + 26 * '-') print('\nPerforming system simulation, this might take some time ...') if not type(self.pesetup.system) is systems.ExplODE: raise NotImplementedError("Until now, this function can only " + \ "be used for systems of type ExplODE.") if x0 == None: raise ValueError("You have to provide an initial value x0 " + \ "to run the simulation.") x0 = np.squeeze(np.asarray(x0)) if np.atleast_1d(x0).shape[0] != self.pesetup.nx: raise ValueError("Wrong dimension for initial value x0.") if tsim == None: tsim = self.pesetup.tu if usim == None: usim = self.pesetup.uN if psim == None: try: psim = self.phat except AttributeError: errmsg = ''' You have to either perform a parameter estimation beforehand to obtain a parameter set that can be used for simulation, or you have to provide a parameter set in the argument psim. ''' raise AttributeError(errmsg) else: if not np.atleast_1d(np.squeeze(psim)).shape[0] == self.pesetup.np: raise ValueError("Wrong dimension for parameter set psim.") fp = ca.MXFunction("fp", \ [self.pesetup.system.t, self.pesetup.system.u, \ self.pesetup.system.x, self.pesetup.system.eps_e, \ self.pesetup.system.eps_u, self.pesetup.system.p], \ [self.pesetup.system.f]) fpeval = fp([\ self.pesetup.system.t, self.pesetup.system.u, \ self.pesetup.system.x, np.zeros(self.pesetup.neps_e), \ np.zeros(self.pesetup.neps_u), psim])[0] fsim = ca.MXFunction("fsim", \ ca.daeIn(t = self.pesetup.system.t, \ x = self.pesetup.system.x, \ p = self.pesetup.system.u), \ ca.daeOut(ode = fpeval)) Xsim = [] Xsim.append(x0) u0 = ca.DMatrix() for k,e in enumerate(tsim[:-1]): try: integrator = ca.Integrator("integrator", method, \ fsim, {"t0": e, "tf": tsim[k+1]}) except RuntimeError as err: errmsg = ''' It seems like you want to use an integration method that is not currently supported by CasADi. Please refer to the CasADi documentation for a list of supported integrators, or use the default RK4-method by not setting the method-argument of the function. ''' raise RuntimeError(errmsg) if not self.pesetup.nu == 0: u0 = usim[:,k] Xk_end = itemgetter('xf')(integrator({'x0':x0,'p':u0})) Xsim.append(Xk_end) x0 = Xk_end self.Xsim = ca.horzcat(Xsim) print( \ '''System simulation finished.''')
def __init__(self, system = None, \ tu = None, uN = None, \ ty = None, yN = None, \ wv = None, weps_e = None, weps_u = None, \ pinit = None, \ xinit = None, \ linear_solver = None, \ scheme = None, \ order = None): intro.pecas_intro() print('\n' + 22 * '-' + \ ' PECas parameter estimation setup ' + 22 * '-') print('\nStarting parameter estimation problem setup ...') self.linear_solver = linear_solver if type(system) is systems.BasicSystem: self.pesetup = setups.BSsetup(system = system, \ tu = tu, uN = uN, \ pinit = pinit) elif type(system) is systems.ExplODE: self.pesetup = setups.ODEsetup(system = system, \ tu = tu, uN = uN, \ ty = ty, yN = yN, \ pinit = pinit, \ xinit = xinit, \ scheme = scheme, \ order = order) else: raise NotImplementedError( \ "The system type provided by the user is not supported.") # Store the parameter estimation problem setup # self.pesetup = pesetup # Check if the supported measurement data fits to the dimensions of # the output function yN = np.atleast_2d(yN) if yN.shape == (self.pesetup.tu.size, self.pesetup.nphi): yN = yN.T if not yN.shape == (self.pesetup.nphi, self.pesetup.tu.size): raise ValueError(''' The dimension of the measurement data given in yN does not match the dimension of output function and/or tu. Valid dimensions for yN for the given data are: {0} or {1}, but you supported yN of dimension: {2}.'''.format(str((self.pesetup.tu.size, self.pesetup.nphi)), \ str((self.pesetup.nphi, self.pesetup.tu.size)), str(yN.shape))) # Check if the supported standard deviations fit to the dimensions of # the measurement data wv = np.atleast_2d(wv) if wv.shape == yN.T.shape: wv = wv.T if not wv.shape == yN.shape: raise ValueError(''' The dimension of weights of the measurement errors given in wv does not match the dimensions of the measurement data. Valid dimensions for wv for the given data are: {0} or {1}, but you supported wv of dimension: {2}.'''.format(str(yN.shape), str(yN.T.shape), str(wv.shape))) # Get the measurement values and standard deviations into the # necessary order of apperance and dimensions self.yN = np.zeros(np.size(yN)) self.wv = np.zeros(np.size(yN)) for k in range(yN.shape[0]): self.yN[k:yN.shape[0]*yN.shape[1]+1:yN.shape[0]] = \ yN[k, :] self.wv[k:yN.shape[0]*yN.shape[1]+1:yN.shape[0]] = \ wv[k, :] self.weps_e = [] try: if self.pesetup.neps_e != 0: weps_e = np.atleast_2d(weps_e) try: if weps_e.shape == (1, self.pesetup.neps_e): weps_e = weps_e.T if not weps_e.shape == (self.pesetup.neps_e, 1): raise ValueError(''' The dimensions of the weights of the equation errors given in weps_e does not match the dimensions of the equation errors given in eps_e.''') self.weps_e = weps_e except AttributeError: pass try: self.weps_e = np.squeeze(ca.repmat(weps_e, self.pesetup.nsteps * \ (len(self.pesetup.tauroot)-1), 1)) except AttributeError: self.weps_e = [] except AttributeError: pass self.weps_u = [] try: if self.pesetup.neps_u != 0: weps_u = np.atleast_2d(weps_u) try: if weps_u.shape == (1, self.pesetup.neps_u): weps_u = weps_u.T if not weps_u.shape == (self.pesetup.neps_u, 1): raise ValueError(''' The dimensions of the weights of the input errors given in weps_u does not match the dimensions of the input errors given in eps_u.''') self.weps_u = weps_u except AttributeError: pass try: self.weps_u = np.squeeze(ca.repmat(weps_u, self.pesetup.nsteps * \ (len(self.pesetup.tauroot)-1), 1)) except AttributeError: self.weps_u = [] except AttributeError: pass # Set up the covariance matrix for the measurements # self.w = ca.diag(np.concatenate((self.wv, self.weps_e,self.weps_u))) self.w = ca.veccat((self.wv, self.weps_e,self.weps_u)) print('Setup of the parameter estimation problem sucessful.')
def run_parameter_estimation(self, hessian = "gauss-newton"): r''' :param hessian: Method of hessian calculation/approximation; possible values are `gauss-newton` and `exact-hessian` :type hessian: str This functions will run a least squares parameter estimation for the given problem and data set. For this, an NLP of the following structure is set up with a direct collocation approach and solved using IPOPT: .. math:: \begin{aligned} & \text{arg}\,\underset{x, p, v, \epsilon_e, \epsilon_u}{\text{min}} & & \frac{1}{2} \| R(w, v, \epsilon_e, \epsilon_u) \|_2^2 \\ & \text{subject to:} & & R(w, v, \epsilon_e, \epsilon_u) = w^{^\mathbb{1}/_\mathbb{2}} \begin{pmatrix} {v} \\ {\epsilon_e} \\ {\epsilon_u} \end{pmatrix} \\ & & & w = \begin{pmatrix} {w_{v}}^T & {w_{\epsilon_{e}}}^T & {w_{\epsilon_{u}}}^T \end{pmatrix} \\ & & & v_{l} + y_{l} - \phi(t_{l}, u_{l}, x_{l}, p) = 0 \\ & & & (t_{k+1} - t_{k}) f(t_{k,j}, u_{k,j}, x_{k,j}, p, \epsilon_{e,k,j}, \epsilon_{u,k,j}) - \sum_{r=0}^{d} \dot{L}_r(\tau_j) x_{k,r} = 0 \\ & & & x_{k+1,0} - \sum_{r=0}^{d} L_r(1) x_{k,r} = 0 \\ & & & t_{k,j} = t_k + (t_{k+1} - t_{k}) \tau_j \\ & & & L_r(\tau) = \prod_{r=0,r\neq j}^{d} \frac{\tau - \tau_r}{\tau_j - \tau_r}\\ & \text{for:} & & k = 1, \dots, N, ~~~ l = 1, \dots, M, ~~~ j = 1, \dots, d, ~~~ r = 1, \dots, d \\ & & & \tau_j = \text{time points w. r. t. scheme and order} \end{aligned} The status of IPOPT provides information whether the computation could be finished sucessfully. The optimal values for all optimization variables :math:`\hat{x}` can be accessed via the class variable ``LSq.Xhat``, while the estimated parameters :math:`\hat{p}` can also be accessed separately via the class attribute ``LSq.phat``. **Please be aware:** IPOPT finishing sucessfully does not necessarly mean that the estimation results for the unknown parameters are useful for your purposes, it just means that IPOPT was able to solve the given optimization problem. You have in any case to verify your results, e. g. by simulation using the class function :func:`run_simulation`. ''' intro.pecas_intro() print('\n' + 18 * '-' + \ ' PECas least squares parameter estimation ' + 18 * '-') print(''' Starting least squares parameter estimation using IPOPT, this might take some time ... ''') self.tstart_estimation = time.time() g = ca.vertcat([ca.vec(self.pesetup.phiN) - self.yN + \ ca.vec(self.pesetup.V)]) self.R = ca.sqrt(self.w) * \ ca.veccat([self.pesetup.V, self.pesetup.EPS_E, self.pesetup.EPS_U]) if self.pesetup.g.size(): g = ca.vertcat([g, self.pesetup.g]) self.g = g self.Vars = ca.veccat([ self.pesetup.P, \ self.pesetup.X, \ self.pesetup.XF, \ self.pesetup.V, \ self.pesetup.EPS_E, \ self.pesetup.EPS_U, \ ]) nlp = ca.MXFunction("nlp", ca.nlpIn(x=self.Vars), \ ca.nlpOut(f=(0.5 * ca.mul([self.R.T, self.R])), g=self.g)) options = {} options["tol"] = 1e-10 options["linear_solver"] = self.linear_solver if hessian == "gauss-newton": # ipdb.set_trace() gradF = nlp.gradient() jacG = nlp.jacobian("x", "g") # Can't the following be implemented more efficiently?! # gradF.derivative(0, 1) J = ca.jacobian(self.R, self.Vars) sigma = ca.MX.sym("sigma") hessLag = ca.MXFunction("H", \ ca.hessLagIn(x = self.Vars, lam_f = sigma), \ ca.hessLagOut(hess = sigma * ca.mul(J.T, J))) options["hess_lag"] = hessLag options["grad_f"] = gradF options["jac_g"] = jacG elif hessian == "exact-hessian": # let NlpSolver-class compute everything pass else: raise NotImplementedError( \ "Requested method is not implemented. Availabe methods " + \ "are 'gauss-newton' (default) and 'exact-hessian'.") # Initialize the solver, solve the optimization problem solver = ca.NlpSolver("solver", "ipopt", nlp, options) # Store the results of the computation Varsinit = ca.veccat([ self.pesetup.Pinit, \ self.pesetup.Xinit, \ self.pesetup.XFinit, \ self.pesetup.Vinit, \ self.pesetup.EPS_Einit, \ self.pesetup.EPS_Uinit, \ ]) sol = solver(x0 = Varsinit, lbg = 0, ubg = 0) self.Varshat = sol["x"] R_squared_fcn = ca.MXFunction("R_squared_fcn", [self.Vars], [ca.mul([ \ ca.veccat([self.pesetup.V, self.pesetup.EPS_E, self.pesetup.EPS_U]).T, ca.veccat([self.pesetup.V, self.pesetup.EPS_E, self.pesetup.EPS_U])])]) [self.R_squared] = R_squared_fcn([self.Varshat]) self.tend_estimation = time.time() self.duration_estimation = self.tend_estimation - \ self.tstart_estimation print(''' Parameter estimation finished. Check IPOPT output for status information.''')