├── .gitignore ├── .idea ├── .gitignore ├── efficient-calibration-embedded-MPC.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── other.xml └── vcs.xml ├── GLIS_BO_analysis.py ├── GLIS_BO_main.py ├── LICENSE.md ├── README.md ├── idwgopt ├── __init__.py ├── idwgopt.py ├── idwgopt_default.py ├── idwgopt_init.py └── readme.txt ├── kalman.py ├── objective_function.py ├── paper ├── fig │ ├── BEST_GLIS_PC.pdf │ ├── BEST_GLIS_PI.pdf │ ├── ITER_GLIS_PC.pdf │ ├── ITER_GLIS_PC_500.pdf │ ├── ITER_GLIS_PI_500.pdf │ ├── MPC_CPUTIME_GLIS_PI.pdf │ ├── cart_pole.pdf │ └── sym │ │ ├── F.svg │ │ ├── elle.svg │ │ ├── elleM.svg │ │ ├── m (Case Conflict).svg │ │ ├── m.svg │ │ ├── p.svg │ │ └── preview.svg ├── ms.bib ├── ms.pdf └── ms.tex ├── pendulum_MPC_sim.py ├── pendulum_model.py ├── pyMPC ├── __init__.py └── mpc.py └── slides ├── biblio.bib ├── img ├── GLIS │ ├── GLIS_06.pdf │ ├── GLIS_07.pdf │ ├── GLIS_08.pdf │ ├── acquisition.png │ └── surrogate.png ├── MPCloop.pdf ├── MPCloop.svg ├── example │ ├── BEST_GLIS_PC.pdf │ ├── BEST_GLIS_PI.pdf │ ├── COMPUTATION_BO_PI.pdf │ ├── COMPUTATION_GLIS_PI.pdf │ ├── HIL_PC.pdf │ ├── HIL_PI.pdf │ ├── ITER_GLIS_PC_500.pdf │ ├── ITER_GLIS_PI_500.pdf │ └── cart_pole.pdf └── teaser.pdf ├── preamble.tex ├── presentation_main.pdf └── presentation_main.tex /.gitignore: -------------------------------------------------------------------------------- 1 | /idwgopt/.pytest_cache/ 2 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /.idea/efficient-calibration-embedded-MPC.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 40 | 41 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /GLIS_BO_analysis.py: -------------------------------------------------------------------------------- 1 | import os 2 | import matplotlib 3 | if os.name == 'posix' and "DISPLAY" not in os.environ: 4 | matplotlib.use("Agg") 5 | 6 | import numpy as np 7 | from pendulum_MPC_sim import simulate_pendulum_MPC, get_parameter 8 | from numpy.random import seed 9 | import matplotlib.pyplot as plt 10 | from objective_function import f_x, get_simoptions_x 11 | from pendulum_model import RAD_TO_DEG 12 | import pickle 13 | import os 14 | from scipy.interpolate import interp1d 15 | if __name__ == '__main__': 16 | 17 | matplotlib.rc('text', usetex=True) 18 | 19 | algo = 'GLIS' # GLIS or BO 20 | machine = 'PI' # PC or PI 21 | eps_calc = 1.0 22 | iter_max_plot = 500 23 | 24 | plt.close('all') 25 | res_filename = f"res_slower{eps_calc:.0f}_500iter_{algo}_{machine}.pkl" 26 | results = pickle.load(open(res_filename, "rb")) 27 | 28 | 29 | # In[] 30 | FIG_FOLDER = 'fig' 31 | if not os.path.isdir(FIG_FOLDER): 32 | os.makedirs(FIG_FOLDER) 33 | 34 | # In[Re-simulate] 35 | 36 | ## Re-simulate with the optimal point 37 | x_opt = results['x_opt'] 38 | 39 | simopt = get_simoptions_x(x_opt) 40 | t_ref_vec = np.array([0.0, 5.0, 10.0, 13.0, 20.0, 22.0, 25.0, 30.0, 35.0, 40.0, 100.0]) 41 | p_ref_vec = np.array([0.0, 0.4, 0.0, 0.9, 0.9, 0.4, 0.4, 0.4, 0.0, 0.0, 0.0]) 42 | rp_fun = interp1d(t_ref_vec, p_ref_vec, kind='linear') 43 | def xref_fun_def(t): 44 | return np.array([rp_fun(t), 0.0, 0.0, 0.0]) 45 | simopt['xref_fun'] = xref_fun_def 46 | 47 | 48 | simout = simulate_pendulum_MPC(simopt) 49 | 50 | t = simout['t'] 51 | x = simout['x'] 52 | u = simout['u'] 53 | y = simout['y'] 54 | y_meas = simout['y_meas'] 55 | x_ref = simout['x_ref'] 56 | x_MPC_pred = simout['x_MPC_pred'] 57 | x_fast = simout['x_fast'] 58 | x_ref_fast = simout['x_ref_fast'] 59 | y_ref = x_ref[:, [0, 2]] # on-line predictions from the Kalman Filter 60 | uref = get_parameter({}, 'uref') 61 | u_fast = simout['u_fast'] 62 | 63 | t_int = simout['t_int_fast'] 64 | t_fast = simout['t_fast'] 65 | t_calc = simout['t_calc'] 66 | 67 | fig, axes = plt.subplots(3, 1, figsize=(8, 6)) 68 | # axes[0].plot(t, y_meas[:, 0], "r", label='p_meas') 69 | axes[0].plot(t_fast, x_fast[:, 0], "k", label='$p$') 70 | axes[0].plot(t, y_ref[:, 0], "r--", label="$p^{\mathrm{ref}}$", linewidth=2) 71 | axes[0].set_ylim(-0.2, 1.0) 72 | axes[0].set_ylabel("Position (m)") 73 | 74 | # axes[1].plot(t, y_meas[:, 1] * RAD_TO_DEG, "r", label='phi_meas') 75 | axes[1].plot(t_fast, x_fast[:, 2] * RAD_TO_DEG, 'k', label="$\phi$") 76 | idx_pred = 0 77 | axes[1].set_ylim(-12, 12) 78 | axes[1].set_ylabel("Angle (deg)") 79 | 80 | axes[2].plot(t, u[:, 0], 'k', label="$u$") 81 | #axes[2].plot(t, uref * np.ones(np.shape(t)), "r--", label="u_ref") 82 | axes[2].set_ylim(-8, 8) 83 | axes[2].set_ylabel("Force (N)") 84 | axes[2].set_xlabel("Simulation time (s)") 85 | 86 | for ax in axes: 87 | ax.grid(True) 88 | ax.legend(loc='upper right') 89 | 90 | fig_name = f"BEST_{algo}_{machine}.pdf" 91 | fig_path = os.path.join(FIG_FOLDER, fig_name) 92 | fig.savefig(fig_path, bbox_inches='tight') 93 | 94 | # MPC time check 95 | # In[MPC computation time ] 96 | fig, axes = plt.subplots(4, 1, figsize=(14, 10), sharex=True) 97 | axes[0].plot(t, y_meas[:, 0], "r", label='$p_{meas}$') 98 | axes[0].plot(t_fast, x_fast[:, 0], "k", label='$p$') 99 | axes[0].step(t, y_ref[:, 0], "k--", where='post', label="$p_{ref}$") 100 | axes[0].set_ylim(-0.2, 1.0) 101 | axes[0].set_xlabel("Simulation time (s)") 102 | axes[0].set_ylabel("Position (m)") 103 | 104 | axes[1].step(t, t_calc[:, 0] * 1e3, "b", where='post', label='$T_{MPC}$') 105 | axes[1].set_xlabel("Simulation time (s)") 106 | axes[1].set_ylabel("MPC time (ms)") 107 | axes[1].set_ylim(0, 40) 108 | axes[2].step(t_fast[1:], t_int[1:, 0] * 1e3, "b", where='post', label='$T_{ODE}$') 109 | axes[2].set_xlabel("Simulation time (s)") 110 | axes[2].set_ylabel("ODE time (ms)") 111 | axes[2].set_ylim(0, 2) 112 | axes[3].step(t, u[:, 0], where='post', label="$F$") 113 | axes[3].step(t_fast, u_fast[:, 0], where='post', label="$F_{d}$") 114 | axes[3].set_xlabel("Simulation time (s)") 115 | axes[3].set_ylabel("Force (N)") 116 | 117 | for ax in axes: 118 | ax.grid(True) 119 | ax.legend() 120 | 121 | fig_name = f"MPC_CPUTIME_{algo}_{machine}.pdf" 122 | fig_path = os.path.join(FIG_FOLDER, fig_name) 123 | fig.savefig(fig_path, bbox_inches='tight') 124 | 125 | # In[Iteration plot] 126 | 127 | J = results['J_sample'] 128 | Ts_MPC = simout['Ts_MPC'] 129 | 130 | J_best_curr = np.zeros(np.shape(J)) 131 | J_best_val = J[0] 132 | iter_best_val = 0 133 | 134 | fig, axes = plt.subplots(1, 1, figsize=(6, 4)) 135 | axes = [axes] 136 | for i in range(len(J_best_curr)): 137 | if J[i] < J_best_val: 138 | J_best_val = J[i] 139 | iter_best_val = i 140 | J_best_curr[i] = J_best_val 141 | 142 | N = len(J) 143 | iter = np.arange(1, N + 1, dtype=np.int) 144 | axes[0].plot(iter, J, 'k*', label='Current test point') 145 | # axes[0].plot(iter, Y_best_curr, 'r', label='Current best point') 146 | axes[0].plot(iter, J_best_val * np.ones(J.shape), '-', label='Overall best point', color='red') 147 | axes[0].set_xlabel("Iteration index $n$ (-)") 148 | axes[0].set_ylabel(r"Performance cost $\tilde {J}^{\mathrm{cl}}$") 149 | 150 | for ax in axes: 151 | ax.grid(True) 152 | ax.legend(loc='upper right') 153 | 154 | axes[0].set_xlim((0, iter_max_plot)) 155 | axes[0].set_ylim((-1, 25)) 156 | 157 | fig_name = f"ITER_{algo}_{machine}_{iter_max_plot:.0f}.pdf" 158 | fig_path = os.path.join(FIG_FOLDER, fig_name) 159 | fig.savefig(fig_path, bbox_inches='tight') 160 | 161 | 162 | # In[Recompute optimum] 163 | #print(Y_best_val) 164 | 165 | J_opt = f_x(x_opt, eps_calc=results['eps_calc']) 166 | #print(J_opt) 167 | 168 | # In[Optimization computation time] 169 | 170 | t_unknown = results['time_iter'] - ( 171 | results['time_f_eval'] + results['time_opt_acquisition'] + results['time_fit_surrogate']) 172 | fig, ax = plt.subplots(1, 1, figsize=(6, 4)) 173 | ax.step(iter, results['time_iter'], 'k', where='post', label='Total') 174 | ax.step(iter, results['time_f_eval'], 'r', where='post', label='Eval') 175 | ax.step(iter, results['time_opt_acquisition'], 'y', where='post', label='Opt') 176 | ax.step(iter, results['time_fit_surrogate'], 'g', where='post', label='Fit') 177 | ax.grid(True) 178 | ax.legend() 179 | 180 | fig, ax = plt.subplots(1, 1, figsize=(6, 4)) 181 | ax.step(iter, np.cumsum(results['time_iter']), 'k', where='post', label='Total') 182 | ax.step(iter, np.cumsum(results['time_f_eval']), 'r', where='post', label='Function evaluation') 183 | ax.step(iter, np.cumsum(results['time_fit_surrogate']), 'g', where='post', label='Surrogate fitting') 184 | ax.step(iter, np.cumsum(results['time_opt_acquisition']), 'y', where='post', label='Surrogate optimization') 185 | # ax.step(iter, np.cumsum(t_unknown), 'g', where='post', label='Unknown') 186 | ax.set_xlabel("Iteration index i (-)") 187 | ax.set_ylabel("Comulative computational time (s)") 188 | ax.grid(True) 189 | ax.legend() 190 | 191 | fig_name = f"COMPUTATION_{algo}_{machine}.pdf" 192 | fig_path = os.path.join(FIG_FOLDER, fig_name) 193 | fig.savefig(fig_path, bbox_inches='tight') 194 | 195 | residual_time = np.sum(results['time_iter']) - np.sum(results['time_f_eval']) - np.sum( 196 | results['time_opt_acquisition']) - np.sum(results['time_fit_surrogate']) 197 | -------------------------------------------------------------------------------- /GLIS_BO_main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import matplotlib 3 | 4 | if os.name == 'posix' and "DISPLAY" not in os.environ: 5 | matplotlib.use("Agg") 6 | import GPyOpt 7 | import idwgopt.idwgopt 8 | import numpy as np 9 | from pendulum_MPC_sim import simulate_pendulum_MPC, get_parameter, get_default_parameters 10 | import matplotlib.pyplot as plt 11 | from objective_function import dict_to_x, f_x, get_simoptions_x 12 | import pickle 13 | import time 14 | import numba as nb 15 | 16 | if __name__ == '__main__': 17 | 18 | np.random.seed(0) 19 | # initial random points 20 | 21 | # optimization parameters 22 | # Run the optimization 23 | # n_init = 10 changed to 2*n_var (GLIS default choice) 24 | max_iter = 500 # evaluation budget 25 | max_time = np.inf # time budget 26 | eps = 0.0 # Minimum allows distance between the las two observations 27 | eps_calc = 1.0 28 | 29 | method = "GLIS" # GLIS or BO 30 | machine = 'PC' # PC or PI 31 | 32 | 33 | dict_x0 = { 34 | 'QDu_scale': 0.1, 35 | 'Qy11': 0.1, 36 | 'Qy22': 0.9, 37 | 'Np': 40, 38 | 'Nc_perc': 1.0, 39 | 'Ts_MPC': 25e-3, 40 | 'QP_eps_abs_log': -3, 41 | 'QP_eps_rel_log': -2, 42 | 'Q_kal_11': 0.1, 43 | 'Q_kal_22': 0.4, 44 | 'Q_kal_33': 0.1, 45 | 'Q_kal_44': 0.4, 46 | 'R_kal_11': 0.5, 47 | 'R_kal_22': 0.5 48 | } 49 | 50 | dict_context = {} 51 | 52 | n_var = len(dict_x0) 53 | n_init = 2 * n_var 54 | 55 | x0 = dict_to_x(dict_x0) 56 | 57 | bounds = [ 58 | {'name': 'QDu_scale', 'type': 'continuous', 'domain': (1e-16, 1)}, # 0 59 | {'name': 'Qy11', 'type': 'continuous', 'domain': (1e-16, 1)}, # 1 60 | {'name': 'Qy22', 'type': 'continuous', 'domain': (1e-16, 1)}, # 2 61 | {'name': 'Np', 'type': 'continuous', 'domain': (5, 300)}, # 3 62 | {'name': 'Nc_perc', 'type': 'continuous', 'domain': (0.3, 1)}, # 4 63 | {'name': 'Ts_MPC', 'type': 'continuous', 'domain': (1e-3, 50e-3)}, # 5 64 | {'name': 'QP_eps_abs_log', 'type': 'continuous', 'domain': (-7, -1)}, # 6 65 | {'name': 'QP_eps_rel_log', 'type': 'continuous', 'domain': (-7, -1)}, # 7 66 | {'name': 'Q_kal_11', 'type': 'continuous', 'domain': (1e-16, 1)}, # 8 67 | {'name': 'Q_kal_22', 'type': 'continuous', 'domain': (1e-16, 1)}, # 9 68 | {'name': 'Q_kal_33', 'type': 'continuous', 'domain': (1e-16, 1)}, # 10 69 | {'name': 'Q_kal_44', 'type': 'continuous', 'domain': (1e-16, 1)}, # 11 70 | {'name': 'R_kal_11', 'type': 'continuous', 'domain': (1e-16, 1)}, # 12 71 | {'name': 'R_kal_22', 'type': 'continuous', 'domain': (1e-16, 1)}, # 13 72 | ] 73 | 74 | constraints = [ 75 | # {'name': 'min_time', 'constraint': '-x[:,4]*x[:,6] + 0.1'}, # prediction horizon in seconds large enough 76 | ] 77 | 78 | 79 | def f_x_calc(x): 80 | return f_x(x, eps_calc) 81 | 82 | 83 | feasible_region = GPyOpt.Design_space(space=bounds, 84 | constraints=constraints) # , constraints=constraints_context) 85 | # unfeasible_region = GPyOpt.Design_space(space=bounds) 86 | X_init = GPyOpt.experiment_design.initial_design('random', feasible_region, n_init) 87 | 88 | time_optimization_start = time.perf_counter() 89 | if method == "BO": 90 | myBopt = GPyOpt.methods.BayesianOptimization(f_x_calc, 91 | X=X_init, 92 | domain=bounds, 93 | model_type='GP', 94 | acquisition_type='EI', 95 | normalize_Y=True, 96 | exact_feval=False) 97 | 98 | myBopt.run_optimization(max_iter=max_iter - n_init, max_time=max_time, eps=eps, verbosity=False) 99 | 100 | x_opt = myBopt.x_opt 101 | J_opt = myBopt.fx_opt 102 | X_sample = myBopt.X 103 | J_sample = myBopt.Y 104 | idx_opt = np.argmin(J_sample) 105 | 106 | if method == "GLIS": 107 | 108 | # IDWGOPT initialization 109 | nvars = len(bounds) 110 | lb = np.zeros((nvars, 1)).flatten("c") 111 | ub = lb.copy() 112 | for i in range(0, nvars): 113 | lb[i] = bounds[i]['domain'][0] 114 | ub[i] = bounds[i]['domain'][1] 115 | 116 | problem = idwgopt.idwgopt.default(nvars) 117 | problem["nsamp"] = n_init 118 | problem["maxevals"] = max_iter 119 | # problem["g"] = lambda x: np.array([-x[5]*x[7]+0.1]) 120 | problem["lb"] = lb 121 | problem["ub"] = ub 122 | problem["f"] = f_x_calc 123 | problem["useRBF"] = 1 # use Radial Basis Functions 124 | # problem["useRBF"] = 0 # Inverse Distance Weighting 125 | if problem["useRBF"]: 126 | epsil = .5 127 | 128 | 129 | @nb.guvectorize([(nb.float64[:], nb.float64[:], nb.float64[:])], '(n),(n)->()') 130 | def fun_rbf(x1, x2, res): 131 | res[0] = 1 / (1 + epsil ** 2 * np.sum((x1 - x2) ** 2)) 132 | 133 | 134 | problem['rbf'] = fun_rbf 135 | 136 | problem["alpha"] = 1 137 | problem["delta"] = .5 138 | 139 | problem["svdtol"] = 1e-6 140 | # problem["globoptsol"] = "direct" 141 | problem["globoptsol"] = "pswarm" 142 | problem["display"] = 0 143 | 144 | problem["scalevars"] = 1 145 | 146 | problem["constraint_penalty"] = 1e3 147 | problem["feasible_sampling"] = False 148 | 149 | problem["shrink_range"] = 0 # 0 = don't shrink lb/ub 150 | 151 | tic = time.perf_counter() 152 | out = idwgopt.idwgopt.solve(problem) 153 | toc = time.perf_counter() 154 | 155 | x_opt = out["xopt"] 156 | J_opt = out["fopt"] 157 | J_sample = out["F"] 158 | X_sample = out["X"] 159 | idx_opt = np.argmin(J_sample, axis=0) 160 | 161 | time_optimization = time.perf_counter() - time_optimization_start 162 | 163 | print(f"J_best_val: {J_opt:.3f}") 164 | 165 | # In[Re-simulate with the optimal point] 166 | 167 | simopt = get_simoptions_x(x_opt) 168 | simout = simulate_pendulum_MPC(simopt) 169 | 170 | tsim = simout['t'] 171 | xsim = simout['x'] 172 | usim = simout['u'] 173 | 174 | x_ref = simout['x_ref'] 175 | uref = get_parameter({}, 'uref') 176 | 177 | fig, axes = plt.subplots(3, 1, figsize=(10, 10)) 178 | axes[0].plot(tsim, xsim[:, 0], "k", label='p') 179 | axes[0].plot(tsim, x_ref[:, 0], "r--", label="p_ref") 180 | axes[0].set_title("Position (m)") 181 | 182 | axes[1].plot(tsim, xsim[:, 2] * 360 / 2 / np.pi, label="phi") 183 | axes[1].plot(tsim, x_ref[:, 2] * 360 / 2 / np.pi, "r--", label="phi_ref") 184 | axes[1].set_title("Angle (deg)") 185 | 186 | axes[2].plot(tsim, usim[:, 0], label="u") 187 | axes[2].plot(tsim, uref * np.ones(np.shape(tsim)), "r--", label="u_ref") 188 | axes[2].set_title("Force (N)") 189 | 190 | for ax in axes: 191 | ax.grid(True) 192 | ax.legend() 193 | 194 | J_best_curr = np.zeros(np.shape(J_sample)) 195 | J_best_val = J_sample[0] 196 | iter_best_val = 0 197 | 198 | fig, axes = plt.subplots(1, 1, figsize=(8, 5)) 199 | axes = [axes] 200 | for i in range(len(J_best_curr)): 201 | if J_sample[i] < J_best_val: 202 | J_best_val = J_sample[i] 203 | iter_best_val = i 204 | J_best_curr[i] = J_best_val 205 | 206 | N = len(J_sample) 207 | iter = np.arange(0, N, dtype=np.int) 208 | axes[0].plot(iter, J_sample, 'k*', label='Current test boint') 209 | axes[0].plot(iter, J_best_curr, 'g', label='Current best point') 210 | axes[0].plot(iter_best_val, J_best_val, 's', label='Overall best point') 211 | 212 | for ax in axes: 213 | ax.grid(True) 214 | ax.legend() 215 | 216 | # In[Re-evaluate optimal controller] 217 | J_opt = f_x(x_opt) 218 | print(J_opt) 219 | 220 | plt.show() 221 | 222 | # In[Store results] 223 | 224 | result = {} 225 | if method == 'BO': 226 | myBopt.f = None # hack to solve the issues pickling the myBopt object 227 | myBopt.objective = None 228 | 229 | results = {'X_sample': myBopt.X, 'J_sample': myBopt.Y, 230 | 'idx_opt': idx_opt, 'x_opt': x_opt, 'J_opt': J_opt, 231 | 'eps_calc': eps_calc, 232 | 'time_iter': myBopt.time_iter, 'time_f_eval': myBopt.time_f_eval, 233 | 'time_opt_acquisition': myBopt.time_opt_acquisition, 'time_fit_surrogate': myBopt.time_fit_surrogate, 234 | 'myBopt': myBopt, 'method': method 235 | } 236 | 237 | if method == 'GLIS': 238 | results = {'X_sample': X_sample, 'J_sample': J_sample, 239 | 'idx_opt': idx_opt, 'x_opt': x_opt, 'J_opt': J_opt, 240 | 'eps_calc': eps_calc, 241 | 'time_iter': out['time_iter'], 'time_f_eval': out['time_f_eval'], 242 | 'time_opt_acquisition': out['time_opt_acquisition'], 'time_fit_surrogate': out['time_fit_surrogate'], 243 | 'out': out, 'method': method 244 | } 245 | 246 | res_filename = f"res_slower{eps_calc:.0f}_{max_iter:.0f}iter_{method}_{machine}.pkl" 247 | 248 | with open(res_filename, "wb") as file: 249 | pickle.dump(results, file) 250 | 251 | 252 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Marco Forgione 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Efficient Calibration of Embedded MPC 2 | 3 | This repository contains the Python code to reproduce the results of the paper [Efficient Calibration of Embedded MPC](https://arxiv.org/pdf/1911.13021) by Marco Forgione, Dario Piga, and Alberto Bemporad. 4 | 5 | The code performs an efficient data-driven MPC calibration by tuning: 6 | 7 | * MPC weight matrices 8 | * MPC sampling time 9 | * Prediction and control horizon 10 | * Kalman filter matrices 11 | * QP solver relative and absolute tolerances 12 | 13 | to optimize a closed-loop objective function , under the constraint that where is the (worst-case) time required to compute the MPC 14 | control low. This constraints guarantees that the controller can run in real-time. 15 | 16 | ## Main scripts: 17 | 18 | The main script to be executed for MPC calibration is 19 | 20 | `` GLIS_BO_main.py`` 21 | 22 | The results of the MPC calibration are saved in the results_*.pkl file 23 | on the disk and are read by the script 24 | 25 | ``GLIS_BO_analysis.py`` 26 | 27 | that produces the relevant plots. 28 | ## Other files: 29 | * ``pendulum_model.py``: dynamic equations of the pendulum 30 | * ``pendulum_MPC_sim``: performs a single closed-loop MPC simulation 31 | * ``objective_function.py``: objective function 32 | * ``kalman.py``: implements a kalman filter 33 | 34 | ## Included dependencies: 35 | * ``pyMPC``: containts the pyMPC library for Model Predictive Control. Copied from branch dev-BO of my repository , 36 | * ``idwgopt``: contains the python version of the GLIS package version 1.1. Copied from 37 | ## Other dependencies: 38 | 39 | Simulations were performed on a Python 3.6 conda environment with 40 | 41 | * numpy 42 | * scipy 43 | * matplotlib 44 | * OSQP (a QP solver used by the MPC controller) 45 | * python-control (used to solve the DARE of the Kalman Filter) 46 | * GPyOpt (for Bayesian Optimization, optional) 47 | 48 | These dependencies may be installed through the commands: 49 | ``` 50 | conda install numpy scipy matplotlib 51 | pip install osqp 52 | pip install control 53 | pip install gpyopt 54 | ``` 55 | ## Citing 56 | 57 | If you find this project useful, we encourage you to 58 | 59 | * Star this repository :star: 60 | * Cite the [paper](https://arxiv.org/pdf/1911.13021) 61 | ``` 62 | @inproceedings{forgione2020efficient, 63 | title={{E}fficient {C}alibration of {E}mbedded {MPC}}, 64 | author={Forgione, Marco and Piga, Dario and Bemporad, Alberto}, 65 | booktitle={Proc. of the 21st IFAC World Congress, Berlin, Germany}, 66 | year={2020} 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /idwgopt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/idwgopt/__init__.py -------------------------------------------------------------------------------- /idwgopt/idwgopt.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import numba as nb 4 | 5 | @nb.guvectorize([(nb.float64[:], nb.float64[:], nb.float64[:])], '(n),(n)->()') 6 | def fast_dist(X1, X2, res): 7 | res[0] = 0.0 8 | n = X1.shape[0] 9 | for i in range(n): 10 | res[0] += (X1[i] - X2[i])**2 11 | 12 | def solve(prob): 13 | """ 14 | Solve global optimization problem using inverse distance 15 | weighting and radial basis functions. 16 | 17 | (C) 2019 A. Bemporad 18 | 19 | sol = idwgopt.solve(prob) solves the global optimization problem 20 | 21 | min f(x) 22 | s.t. lb <= x <=ub, A*x <=b, g(x)<=0 23 | 24 | using the global optimization algorithm described in [1]. The approach is 25 | particularly useful when f(x) is time-consuming to evaluate, as it 26 | attempts at minimizing the number of function evaluations. 27 | 28 | The default problem structure is 29 | 30 | prob = idwgopt.default(nvars) 31 | 32 | where nvars = dimension of optimization vector x. See function idwgopt_default 33 | for a description of all available options. 34 | 35 | The output argument 'out' is a structure reporting the following information: 36 | 37 | out.X: trace of all samples x at which f(x) has been evaluated 38 | out.F: trace of all function evaluations f(x) 39 | out.W: final set of weights (only meaningful for RBFs) 40 | out.xopt: best sample found during search 41 | out.fopt: best value found during search, fopt=f(xopt) 42 | 43 | 44 | Required Python packages: 45 | pyDOE: https://pythonhosted.org/pyDOE/ 46 | nlopt: https://nlopt.readthedocs.io (required only if DIRECT solver is used) 47 | pyswarm: https://pythonhosted.org/pyswarm/ (required only if PSO solver is used) 48 | 49 | [1] A. Bemporad, "Global optimization via inverse weighting and radial 50 | basis functions", arXiv:1906.06498v1, June 15, 2019. 51 | https://arxiv.org/pdf/1906.06498.pdf 52 | 53 | """ 54 | 55 | import idwgopt.idwgopt_init as idwgopt_init 56 | from pyswarm import pso # https://pythonhosted.org/pyswarm/ 57 | 58 | from numpy.linalg import svd 59 | 60 | from numpy import zeros, ones, diag, inf 61 | from numpy import where, maximum, exp 62 | from math import sqrt, atan, pi 63 | import contextlib 64 | import io 65 | 66 | def get_rbf_weights(M, F, NX, svdtol): 67 | # Solve M*W = F using SVD 68 | 69 | U, dS, V = svd(M[0:NX, 0:NX]) 70 | ii = where(dS >= svdtol) 71 | ns = max(ii[0]) + 1 72 | W = (V[0:ns, ].T).dot(diag(1 / dS[0:ns].flatten('C')).dot((U[:, 0:ns].T).dot(F[0:NX]))) 73 | 74 | return W 75 | 76 | def facquisition(xx, X, F, N, alpha, delta, dF, W, rbf, useRBF): 77 | # Acquisition function to minimize to get next sample 78 | 79 | #d = sum(((X[0:N, ] - (ones((N, 1)) * xx)) ** 2).T) 80 | 81 | #d = np.sum((X[0:N, ] - xx) ** 2, axis=-1) 82 | d = fast_dist(X[0:N,:], xx) 83 | 84 | ii = where(d < 1e-12) 85 | if ii[0].size > 0: 86 | fhat = F[ii[0]][0] 87 | dhat = 0 88 | else: 89 | w = exp(-d) / d 90 | sw = np.sum(w) 91 | 92 | if useRBF: 93 | v = rbf(X[0:N,:],xx) 94 | fhat = v.ravel().dot(W.ravel()) 95 | # v = zeros((N, 1)) 96 | # for j in range(N): 97 | # v[j] = rbf(X[j,].T, xx) 98 | # fhat = sum(v * W) 99 | else: 100 | fhat = np.sum(F[0:N, ] * w) / sw 101 | 102 | # dhat = (delta * atan(1 / np.sum(1 / d)) * 2 / pi * dF + 103 | # alpha * sqrt(np.sum(w * (F[0:N, ] - fhat).flatten("c") ** 2) / sw)) 104 | dhat = delta * atan(1 / np.sum(1 / d)) * 2 / pi * dF 105 | f_err = (F[0:N, ] - fhat).flatten("c") 106 | f_err_w = w * f_err**2 107 | dhat += alpha * sqrt(np.sum(f_err_w) / sw) 108 | 109 | f = fhat - dhat 110 | 111 | return f 112 | 113 | (f, lb, ub, nvar, Aineq, bineq, g, isLinConstrained, isNLConstrained, 114 | X, F, z, nsamp, maxevals, epsDeltaF, alpha, delta, rhoC, display, svdtol, 115 | dd, d0, useRBF, rbf, M, scalevars, globoptsol, DIRECTopt, 116 | PSOiters, PSOswarmsize) = idwgopt_init.init(prob) 117 | 118 | 119 | time_iter = [] 120 | time_f_eval = [] 121 | time_opt_acquisition = [] 122 | time_fit_surrogate = [] 123 | 124 | for i in range(nsamp): 125 | time_fun_eval_start = time.perf_counter() 126 | F[i] = f(X[i,].T) 127 | time_fun_eval_i = time.perf_counter() - time_fun_eval_start 128 | time_iter.append(time_fun_eval_i) 129 | time_f_eval.append(time_fun_eval_i) 130 | time_opt_acquisition.append(0.0) 131 | time_fit_surrogate.append(0.0) 132 | 133 | if useRBF: 134 | W = get_rbf_weights(M, F, nsamp, svdtol) 135 | else: 136 | W = [] 137 | 138 | fbest = inf 139 | zbest = zeros((nsamp, 1)) 140 | for i in range(nsamp): 141 | isfeas = True 142 | if isLinConstrained: 143 | isfeas = isfeas and all(Aineq.dot(X[i,].T) <= bineq.flatten("c")) 144 | if isNLConstrained: 145 | isfeas = isfeas and all(g(X[i,]) <= 0) 146 | if isfeas and fbest > F[i]: 147 | fbest = F[i] 148 | zbest = X[i,] 149 | 150 | Fmax = max(F[0:nsamp]) 151 | Fmin = min(F[0:nsamp]) 152 | 153 | N = nsamp 154 | 155 | while N < maxevals: 156 | 157 | time_iter_start = time.perf_counter() 158 | 159 | dF = Fmax - Fmin 160 | 161 | if isLinConstrained or isNLConstrained: 162 | penalty = rhoC * dF 163 | if isLinConstrained and isNLConstrained: 164 | constrpenalty = lambda x: (penalty * (sum(maximum((Aineq.dot(x) - bineq).flatten("c"), 0) ** 2) 165 | + sum(maximum(g(x), 0) ** 2))) 166 | elif isLinConstrained and not isNLConstrained: 167 | constrpenalty = lambda x: penalty * (sum(maximum((Aineq.dot(x) - bineq).flatten("c"), 0) ** 2)) 168 | elif not isLinConstrained and isNLConstrained: 169 | constrpenalty = lambda x: penalty * sum(maximum(g(x), 0) ** 2) 170 | else: 171 | constrpenalty = lambda x: 0 172 | 173 | acquisition = lambda x: (facquisition(x, X, F, N, alpha, delta, dF, W, rbf, useRBF) 174 | + constrpenalty(x)) 175 | 176 | time_opt_acq_start = time.perf_counter() 177 | if globoptsol == "pswarm": 178 | # pso(func, lb, ub, ieqcons=[], f_ieqcons=None, args=(), kwargs={}, 179 | # swarmsize=100, omega=0.5, phip=0.5, phig=0.5, maxiter=100, minstep=1e-8, 180 | # minfunc=1e-8, debug=False) 181 | if display == 0: 182 | with contextlib.redirect_stdout(io.StringIO()): 183 | z, cost = pso(acquisition, lb, ub, swarmsize=PSOswarmsize, 184 | minfunc=dF * 1e-8, maxiter=PSOiters) 185 | else: 186 | z, cost = pso(acquisition, lb, ub, swarmsize=PSOswarmsize, 187 | minfunc=dF * 1e-8, maxiter=PSOiters) 188 | 189 | elif globoptsol == "direct": 190 | DIRECTopt.set_min_objective(lambda x, grad: acquisition(x)[0]) 191 | z = DIRECTopt.optimize(z.flatten("c")) 192 | time_opt_acquisition.append(time.perf_counter() - time_opt_acq_start) 193 | 194 | time_fun_eval_start = time.perf_counter() 195 | fz = f(z) # function evaluation 196 | time_f_eval.append(time.perf_counter() - time_fun_eval_start) 197 | 198 | N = N + 1 199 | 200 | X[N - 1,] = z.T 201 | F[N - 1] = fz 202 | 203 | Fmax = max(Fmax, fz) 204 | Fmin = min(Fmin, fz) 205 | 206 | time_fit_surrogate_start = time.perf_counter() 207 | if useRBF: 208 | # Just update last row and column of M 209 | for h in range(N): 210 | mij = rbf(X[h,], X[N - 1,]) 211 | M[h, N - 1] = mij 212 | M[N - 1, h] = mij 213 | 214 | W = get_rbf_weights(M, F, N, svdtol) 215 | time_fit_surrogate.append(time.perf_counter() - time_fit_surrogate_start) 216 | 217 | if fbest > fz: 218 | fbest = fz.copy() 219 | zbest = z.copy() 220 | 221 | if display > 0: 222 | 223 | print("N = %4d, cost = %8g, best = %8g" % (N, fz, fbest)) 224 | 225 | string = "" 226 | for j in range(nvar): 227 | aux = zbest[j] 228 | if scalevars: 229 | aux = aux * dd[j] + d0[j] 230 | 231 | string = string + " x" + str(j + 1) + " = " + str(aux) 232 | print(string) 233 | 234 | time_iter.append(time.perf_counter() - time_iter_start) 235 | 236 | # end while 237 | 238 | xopt = zbest.copy() 239 | if scalevars: 240 | # Scale variables back 241 | xopt = xopt * dd + d0 242 | X = X * (ones((N, 1)) * dd) + ones((N, 1)) * d0 243 | 244 | fopt = fbest.copy() 245 | 246 | if not useRBF: 247 | W = [] 248 | 249 | out = {"xopt": xopt, 250 | "fopt": fopt, 251 | "X": X, 252 | "F": F, 253 | "W": W, 254 | 'time_iter': np.array(time_iter), 255 | 'time_opt_acquisition': np.array(time_opt_acquisition), 256 | 'time_fit_surrogate': np.array(time_fit_surrogate), 257 | 'time_f_eval': np.array(time_f_eval)} 258 | 259 | return out 260 | 261 | 262 | def default(nvars): 263 | """ Generate default problem structure for IDW-RBF Global Optimization. 264 | 265 | problem=idwgopt.default(n) generate a default problem structure for a 266 | an optimization with n variables. 267 | 268 | (C) 2019 by A. Bemporad. 269 | """ 270 | 271 | import idwgopt.idwgopt_default as idwgopt_default 272 | problem = idwgopt_default.set(nvars) 273 | 274 | return problem 275 | -------------------------------------------------------------------------------- /idwgopt/idwgopt_default.py: -------------------------------------------------------------------------------- 1 | def set(nvars): 2 | """ 3 | Generate default problem structure for IDW-RBF Global Optimization. 4 | 5 | problem = idwgopt_default.set(n) generate a default problem structure for a 6 | an optimization with n variables. 7 | 8 | (C) 2019 A. Bemporad, July 6, 2019 9 | """ 10 | 11 | from numpy import zeros, ones 12 | from numpy import sum as vecsum 13 | 14 | problem = { 15 | "f": "[set cost function here]", # cost function to minimize 16 | "lb": -1 * ones((nvars, 1)), # lower bounds on the optimization variables 17 | "ub": 1 * ones((nvars, 1)), # upper bounds on the optimization variables 18 | "maxevals": 20, # maximum number of function evaluations 19 | "alpha": 1, # weight on function uncertainty variance measured by IDW 20 | "delta": 0.5, # weight on distance from previous samples 21 | "nsamp": 2*nvars, # number of initial samples 22 | "useRBF": 1, # 1 = use RBFs, 0 = use IDW interpolation 23 | "rbf": lambda x1,x2: 1/(1+0.25*vecsum((x1-x2)**2)), # inverse quadratic 24 | # RBF function (only used if useRBF=1) 25 | "scalevars": 1, # scale problem variables 26 | "svdtol": 1e-6, # tolerance used to discard small singular values 27 | "Aineq": zeros((0,nvars)), # matrix A defining linear inequality constraints 28 | "bineq": zeros((0,1)), # right hand side of constraints A*x <= b 29 | "g": 0, # constraint function. Example: problem.g = lambda x: x[0]**2+x[1]**2-1 30 | "shrink_range": 1, # flag. If 0, disable shrinking lb and ub to bounding box of feasible set 31 | "constraint_penalty": 1000, # penalty term on violation of linear inequality 32 | # and nonlinear constraint 33 | "feasible_sampling": False, # if True, initial samples are forced to be feasible 34 | "globoptsol": "direct", # nonlinear solver used during acquisition. 35 | # interfaced solvers are: 36 | # "direct" DIRECT from NLopt tool (nlopt.readthedocs.io) 37 | # "pswarm" PySwarm solver (pythonhosted.org/pyswarm/) 38 | "display": 0, # verbosity level (0=minimum) 39 | "PSOiters": 500, # number of iterations in PSO solver 40 | "PSOswarmsize": 20, # swarm size in PSO solver 41 | "epsDeltaF": 1e-4 # minimum value used to scale the IDW distance function 42 | } 43 | 44 | return problem 45 | -------------------------------------------------------------------------------- /idwgopt/idwgopt_init.py: -------------------------------------------------------------------------------- 1 | def init(prob): 2 | """ 3 | Init function for idwgopt.py 4 | 5 | (C) 2019 A. Bemporad, July 6, 2019 6 | """ 7 | 8 | from pyDOE import lhs #https://pythonhosted.org/pyDOE/ 9 | # import nlopt # https://nlopt.readthedocs.io 10 | from pyswarm import pso # https://pythonhosted.org/pyswarm/ 11 | 12 | from scipy.optimize import linprog as linprog 13 | 14 | from numpy import size, zeros, ones, diag 15 | from numpy import where, maximum 16 | from math import ceil 17 | import sys 18 | import contextlib 19 | import io 20 | 21 | 22 | # input arguments 23 | f0 = prob["f"] 24 | lb = prob["lb"].copy() 25 | ub = prob["ub"].copy() 26 | maxevals = prob["maxevals"] 27 | alpha = prob["alpha"] 28 | delta = prob["delta"] 29 | nsamp = prob["nsamp"] 30 | useRBF = prob["useRBF"] 31 | rbf = prob["rbf"] 32 | scalevars = prob["scalevars"] 33 | svdtol = prob["svdtol"] 34 | Aineq = prob["Aineq"].copy() 35 | bineq = prob["bineq"].copy() 36 | g0 = prob["g"] 37 | shrink_range = prob["shrink_range"] 38 | rhoC = prob["constraint_penalty"] 39 | feasible_sampling = prob["feasible_sampling"] 40 | globoptsol = prob["globoptsol"] 41 | display = prob["display"] 42 | PSOiters = prob["PSOiters"] 43 | PSOswarmsize = prob["PSOswarmsize"] 44 | epsDeltaF = prob["epsDeltaF"] 45 | 46 | nvar = size(lb) # number of optimization variables 47 | 48 | isLinConstrained = (size(bineq) > 0) 49 | isNLConstrained = (g0 != 0) 50 | if not isLinConstrained and not isNLConstrained: 51 | feasible_sampling = False 52 | 53 | f = f0 54 | g = g0 55 | if scalevars: 56 | # Rescale problem variables in [-1,1] 57 | dd = (ub-lb)/2 58 | d0 = (ub+lb)/2 59 | f = lambda x: f0(x*dd+d0) 60 | 61 | lb = -ones(nvar) 62 | ub = ones(nvar) 63 | 64 | if isLinConstrained: 65 | bineq = bineq-Aineq.dot(d0) 66 | Aineq = Aineq.dot(diag(dd.flatten('C'))) 67 | 68 | if isNLConstrained: 69 | g = lambda x: g0(x*dd+d0) 70 | 71 | # set solver options 72 | if globoptsol=="pswarm": 73 | # nothing to do 74 | pass 75 | DIRECTopt = [] 76 | 77 | elif globoptsol=="direct": 78 | #DIRECTopt = nlopt.opt(nlopt.GN_DIRECT_L, 2) 79 | DIRECTopt = nlopt.opt(nlopt.GN_DIRECT, 2) 80 | DIRECTopt.set_lower_bounds(lb.flatten("C")) 81 | DIRECTopt.set_upper_bounds(ub.flatten("C")) 82 | DIRECTopt.set_ftol_abs(1e-5) 83 | DIRECTopt.set_maxeval(2000) 84 | DIRECTopt.set_xtol_rel(1e-5) 85 | 86 | else: 87 | print("Unknown solver") 88 | sys.exit(1) 89 | 90 | 91 | if shrink_range == 1: 92 | # possibly shrink lb,ub to constraints 93 | if not isNLConstrained and isLinConstrained: 94 | flin=zeros((nvar,1)) 95 | 96 | for i in range(nvar): 97 | flin[i]=1 98 | res=linprog(flin, Aineq, bineq, bounds=(None,None)) 99 | aux=max(lb[i],res.fun) 100 | lb[i]=aux 101 | flin[i]=-1 102 | res=linprog(flin, Aineq, bineq, bounds=(None,None)) 103 | aux=min(ub[i],-res.fun) 104 | ub[i]=aux 105 | flin[i]=0 106 | 107 | elif isNLConstrained: 108 | NLpenaltyfun = lambda x: sum(maximum(g(x),0)**2) 109 | if isLinConstrained: 110 | LINpenaltyfun = lambda x: sum(maximum((Aineq.dot(x)-bineq).flatten("c"),0)**2) 111 | else: 112 | LINpenaltyfun = lambda x: 0 113 | 114 | for i in range(0,nvar): 115 | obj_fun = lambda x: x[i] + 1.0e4*(NLpenaltyfun(x) + LINpenaltyfun(x)) 116 | if globoptsol=="pswarm": 117 | if display == 0: 118 | with contextlib.redirect_stdout(io.StringIO()): 119 | z, cost = pso(obj_fun, lb, ub, swarmsize=30, 120 | minfunc=1e-8, maxiter=2000) 121 | else: 122 | z, cost = pso(obj_fun, lb, ub, swarmsize=30, 123 | minfunc=1e-8, maxiter=2000) 124 | else: # globoptsol=="direct": 125 | DIRECTopt.set_min_objective(lambda x,grad: obj_fun(x)[0]) 126 | z = DIRECTopt.optimize(z.flatten("c")) 127 | lb[i] = max(lb[i],z[i]) 128 | 129 | obj_fun = lambda x: -x[i] + 1.0e4*(NLpenaltyfun(x) + LINpenaltyfun(x)) 130 | 131 | if globoptsol=="pswarm": 132 | if display == 0: 133 | with contextlib.redirect_stdout(io.StringIO()): 134 | z, cost = pso(obj_fun, lb, ub, swarmsize=30, 135 | minfunc=1e-8, maxiter=2000) 136 | else: 137 | z, cost = pso(obj_fun, lb, ub, swarmsize=30, 138 | minfunc=1e-8, maxiter=2000) 139 | else: # globoptsol=="direct": 140 | DIRECTopt.set_min_objective(lambda x,grad: obj_fun(x)[0]) 141 | z = DIRECTopt.optimize(z.flatten("c")) 142 | ub[i] = min(ub[i],z[i]) 143 | 144 | if maxevals x[k+1|k] 123 | self.y = self.C @ self.x #+ self.D @ u 124 | 125 | def sim(self, u_seq, x=None): 126 | 127 | if x is None: 128 | x = self.x 129 | Np = __first_dim__(u_seq) 130 | nu = __second_dim__(u_seq) 131 | assert(nu == self.nu) 132 | 133 | y = np.zeros((Np,self.ny)) 134 | x_tmp = x 135 | for i in range(Np): 136 | u_tmp = u_seq[i] 137 | y[i,:] = self.C @ x_tmp + self.D @ u_tmp 138 | x_tmp = self.A @ x_tmp + self.B @ u_tmp 139 | 140 | #y[Np] = self.C @ x_tmp + self.D @ u_tmp # not really true for D. Here it is 0 anyways 141 | return y 142 | 143 | 144 | if __name__ == '__main__': 145 | 146 | # Constants # 147 | Ts = 0.2 # sampling time (s) 148 | M = 2 # mass (Kg) 149 | b = 0.3 # friction coefficient (N*s/m) 150 | 151 | Ad = np.array([ 152 | [1.0, Ts], 153 | [0, 1.0 -b/M*Ts] 154 | ]) 155 | 156 | Bd = np.array([ 157 | [0.0], 158 | [Ts/M]]) 159 | 160 | Cd = np.array([[1, 0]]) 161 | Dd = np.array([[0]]) 162 | 163 | [nx, nu] = Bd.shape # number of states and number or inputs 164 | ny = np.shape(Cd)[0] 165 | 166 | ## General design ## 167 | Bd_kal = np.hstack([Bd, Bd]) 168 | Dd_kal = np.array([[0, 0]]) 169 | Q_kal = np.array([[100]]) # nw x nw matrix, w general (here, nw = nu) 170 | R_kal = np.eye(ny) # ny x ny) 171 | L_general,P_general,W_general = kalman_design(Ad, Bd_kal, Cd, Dd_kal, Q_kal, R_kal, type='predictor') 172 | 173 | # Simple design 174 | Q_kal = 10 * np.eye(nx) 175 | R_kal = np.eye(ny) 176 | L_simple,P_simple,W_simple = kalman_design_simple(Ad, Bd, Cd, Dd, Q_kal, R_kal, type='predictor') 177 | 178 | # Simple design written in general form 179 | Bd_kal = np.hstack([Bd, np.eye(nx)]) 180 | Dd_kal = np.hstack([Dd, np.zeros((ny, nx))]) 181 | Q_kal = 10 * np.eye(nx)#np.eye(nx) * 100 182 | R_kal = np.eye(ny) * 1 183 | L_gensim,P_gensim,W_gensim = kalman_design_simple(Ad, Bd, Cd, Dd, Q_kal, R_kal, type='predictor') 184 | 185 | assert(np.isclose(L_gensim[0], L_simple[0])) 186 | 187 | L, _, _ = kalman_design_simple(Ad, Bd, Cd, Dd, Q_kal, R_kal, type='predictor') 188 | x0 = np.zeros(nx) 189 | KF = LinearStateEstimator(x0, Ad, Bd, Cd, Dd, L) 190 | KF.L = L 191 | 192 | L_predictor = Ad@P_simple@np.transpose(Cd)/([Cd@P_simple@np.transpose(Cd)+R_kal]) 193 | L_filter = P_simple@np.transpose(Cd)/([Cd@P_simple@np.transpose(Cd)+R_kal]) 194 | -------------------------------------------------------------------------------- /objective_function.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from pendulum_MPC_sim import simulate_pendulum_MPC, get_parameter, Ts_fast, get_default_parameters 3 | from scipy import sparse 4 | from pendulum_model import * 5 | 6 | N_eval = 0 7 | 8 | VAR_NAMES = ['QDu_scale', 9 | 'Qy11', 10 | 'Qy22', 11 | 'Np', 12 | 'Nc_perc', 13 | 'Ts_MPC', 14 | 'QP_eps_abs_log', 15 | 'QP_eps_rel_log', 16 | 'Q_kal_11', 17 | 'Q_kal_22', 18 | 'Q_kal_33', 19 | 'Q_kal_44', 20 | 'R_kal_11', 21 | 'R_kal_22', 22 | ] 23 | 24 | 25 | def dict_to_x(dict_x): 26 | N_vars1 = len(dict_x) 27 | N_vars2 = len(VAR_NAMES) 28 | assert (N_vars1 == N_vars2) 29 | N_vars = N_vars1 30 | 31 | x = np.zeros(N_vars) 32 | for var_idx in range(N_vars): 33 | x[var_idx] = dict_x[VAR_NAMES[var_idx]] 34 | return x 35 | 36 | 37 | def x_to_dict(x): 38 | if len(x.shape) == 2: 39 | x = x[0] 40 | N_vars1 = len(x) 41 | N_vars2 = len(VAR_NAMES) 42 | assert (N_vars1 == N_vars2) 43 | N_vars = N_vars1 44 | 45 | dict_x = {} 46 | for var_idx in range(N_vars): 47 | dict_x[VAR_NAMES[var_idx]] = x[var_idx] 48 | return dict_x 49 | 50 | 51 | def get_simoptions_x(x): 52 | so = x_to_dict(x) # simopt 53 | 54 | # MPC cost: weight matrices 55 | Qx = sparse.diags([so['Qy11'], 0, so['Qy22'], 0]) # /sum_MPC_Qy # Quadratic cost for states x0, x1, ..., x_N-1 56 | QxN = Qx 57 | QDu = so['QDu_scale'] * sparse.eye(1) # Quadratic cost for Du0, Du1, ...., Du_N-1 58 | 59 | so['Qx'] = Qx 60 | so['QxN'] = QxN 61 | so['Qu'] = 0.0 * sparse.eye(1) 62 | so['QDu'] = QDu 63 | 64 | # MPC cost: prediction and control horizon 65 | so['Np'] = np.int(round(so['Np'])) 66 | so['Nc'] = np.int(round(so['Np'] * so['Nc_perc'])) 67 | 68 | # MPC cost: sample time 69 | # Make Ts_MPC a multiple of Ts_fast 70 | Ts_MPC = so['Ts_MPC'] 71 | Ts_MPC = ((Ts_MPC // Ts_fast)) * Ts_fast # make Ts_MPC an integer multiple of Ts_fast 72 | so['Ts_MPC'] = Ts_MPC 73 | 74 | # MPC: solver settings 75 | so['QP_eps_abs'] = 10 ** so['QP_eps_abs_log'] 76 | so['QP_eps_rel'] = 10 ** so['QP_eps_rel_log'] 77 | 78 | # Kalman filter: matrices 79 | Q_kal = np.diag([so['Q_kal_11'], so['Q_kal_22'], so['Q_kal_33'], so['Q_kal_44']]) 80 | R_kal = np.diag([so['R_kal_11'], so['R_kal_22']]) 81 | 82 | so['Q_kal'] = Q_kal 83 | so['R_kal'] = R_kal 84 | 85 | # Fixed simulation settings 86 | so['std_nphi'] = 0.01 87 | so['std_npos'] = 0.02 88 | 89 | so['std_dF'] = 0.1 90 | so['w_F'] = 5 91 | 92 | return so 93 | 94 | 95 | def f_x(x, eps_calc=1.0, seed_val=None): 96 | global N_eval 97 | 98 | if seed_val is None: 99 | seed_val = N_eval 100 | 101 | simoptions = get_simoptions_x(x) 102 | simoptions['seed_val'] = seed_val 103 | 104 | sim_failed = False 105 | try: 106 | simout = simulate_pendulum_MPC(simoptions) 107 | except ValueError as e: 108 | print(e) 109 | sim_failed = True 110 | 111 | if not sim_failed: 112 | t = simout['t'] 113 | y_meas = simout['y_meas'] 114 | x_ref = simout['x_ref'] 115 | p_meas = y_meas[:, 0] 116 | phi_meas = y_meas[:, 1] 117 | 118 | p_ref = x_ref[:, 0] 119 | phi_ref = x_ref[:, 2] 120 | 121 | J_perf = 10 * np.mean(np.abs(p_ref - p_meas)) + 0.0 * np.max(np.abs(p_ref - p_meas)) + \ 122 | 30 * np.mean(np.abs(np.abs(phi_ref - phi_meas))) # + 15*np.max(np.abs(np.abs(phi_ref - phi_meas))) 123 | 124 | # Computation of the barrier function 125 | t_calc = simout['t_calc'] 126 | eps_margin = 0.8 127 | 128 | t_calc = eps_calc * t_calc 129 | t_calc_wc = np.max(t_calc) # worst-case computational cost (max computational time) 130 | 131 | Ts_MPC = simout['Ts_MPC'] 132 | t_available = Ts_MPC * eps_margin 133 | 134 | delay_wc = (t_calc_wc - t_available) 135 | delay_wc = delay_wc * (delay_wc >= 0) 136 | J_calc = (delay_wc / t_available) * 1e3 137 | 138 | emergency = simout['emergency_fast'] 139 | emergency_time, _ = np.where(emergency > 0) 140 | if len(emergency_time) > 0: 141 | J_emergency = (len(emergency) - emergency_time[0]) / len(emergency) * 1e3 142 | else: 143 | J_emergency = 0.0 144 | 145 | else: 146 | J_perf = 1e3 147 | J_calc = 1e3 148 | J_emergency = 1e3 # (len(emergency) - emergency_time[0]) / len(emergency) * 1e3 149 | # J_perf = 2e1 150 | # J_fit = 2e1 151 | 152 | J_cl = np.log(J_perf) + np.log(1 + J_calc) + np.log(1+J_emergency) 153 | 154 | print(f"N_eval: {N_eval}, J_perf:{J_perf:.2f}, J_calc:{J_calc:.2f}, J_emergency:{J_emergency:.2f}, J_cl:{J_cl:.2f}") 155 | N_eval += 1 156 | 157 | return J_cl # + J_fit 158 | 159 | 160 | if __name__ == '__main__': 161 | 162 | import matplotlib.pyplot as plt 163 | 164 | dict_x0 = { 165 | 166 | 'QDu_scale': 0.001, 167 | 'Qy11': 0.1, 168 | 'Qy22': 0.5, 169 | 'Np': 100, 170 | 'Nc_perc': 0.5, 171 | 'Ts_MPC': 10e-3, 172 | 'QP_eps_abs_log': -3, 173 | 'QP_eps_rel_log': -3, 174 | 'Q_kal_11': 0.1, 175 | 'Q_kal_22': 0.9, 176 | 'Q_kal_33': 0.1, 177 | 'Q_kal_44': 0.9, 178 | 'R_kal_11': 0.5, 179 | 'R_kal_22': 0.5 180 | } 181 | x0 = dict_to_x(dict_x0) 182 | 183 | f_x0 = x_to_dict(x0) 184 | J_tot = f_x(x0) 185 | 186 | simopt = get_simoptions_x(x0) 187 | simout = simulate_pendulum_MPC(simopt) 188 | 189 | t = simout['t'] 190 | x = simout['x'] 191 | u = simout['u'] 192 | y = simout['y'] 193 | y_meas = simout['y_meas'] 194 | x_ref = simout['x_ref'] 195 | x_MPC_pred = simout['x_MPC_pred'] 196 | x_fast = simout['x_fast'] 197 | x_ref_fast = simout['x_ref_fast'] 198 | y_ref = x_ref[:, [0, 2]] # on-line predictions from the Kalman Filter 199 | uref = get_parameter({}, 'uref') 200 | 201 | fig, axes = plt.subplots(3, 1, figsize=(10, 10)) 202 | axes[0].plot(t, y_meas[:, 0], "b", label='p_meas') 203 | axes[0].plot(t, y[:, 0], "k", label='p') 204 | axes[0].plot(t, y_ref[:, 0], "k--", label="p_ref") 205 | axes[0].set_title("Position (m)") 206 | 207 | axes[1].plot(t, y_meas[:, 1] * RAD_TO_DEG, "b", label='phi_meas') 208 | axes[1].plot(t, y[:, 1] * RAD_TO_DEG, 'k', label="phi") 209 | axes[1].plot(t, y_ref[:, 1] * RAD_TO_DEG, "k--", label="phi_ref") 210 | axes[1].set_title("Angle (deg)") 211 | 212 | axes[2].plot(t, u[:, 0], label="u") 213 | axes[2].plot(t, uref * np.ones(np.shape(t)), "r--", label="u_ref") 214 | axes[2].set_title("Force (N)") 215 | 216 | for ax in axes: 217 | ax.grid(True) 218 | ax.legend() 219 | 220 | default = get_default_parameters(simopt) 221 | 222 | t_calc = simout['t_calc'] 223 | fig, ax = plt.subplots(1, 1, figsize=(5, 5)) 224 | plt.hist(t_calc * 1e3) 225 | plt.title("Computation time (ms)") 226 | 227 | t_int = simout['t_int_fast'] 228 | t_fast = simout['t_fast'] 229 | u_fast = simout['u_fast'] 230 | 231 | # MPC time check 232 | # In[MPC computation time ] 233 | fig, axes = plt.subplots(4, 1, figsize=(14, 10), sharex=True) 234 | axes[0].plot(t, y_meas[:, 0], "b", label='p_meas') 235 | axes[0].plot(t_fast, x_fast[:, 0], "k", label='p') 236 | axes[0].step(t, y_ref[:, 0], "k--", where='post', label="p_ref") 237 | axes[0].set_ylim(-0.2, 1.0) 238 | axes[0].set_xlabel("Simulation time (s)") 239 | axes[0].set_ylabel("Position (m)") 240 | 241 | axes[1].step(t, t_calc[:, 0] * 1e3, "b", where='post', label='T_MPC') 242 | axes[1].set_xlabel("Simulation time (s)") 243 | axes[1].set_ylabel("MPC time (ms)") 244 | axes[1].set_ylim(0, 4) 245 | axes[2].step(t_fast[1:], t_int[1:, 0] * 1e3, "b", where='post', label='T_ODE') # why is 1st slow? check numba 246 | axes[2].set_xlabel("Simulation time (s)") 247 | axes[2].set_ylabel("ODE time (ms)") 248 | axes[2].set_ylim(0, 0.3) 249 | axes[3].step(t, u[:, 0], where='post', label="F") 250 | axes[3].step(t_fast, u_fast[:, 0], where='post', label="F_d") 251 | axes[3].set_xlabel("Simulation time (s)") 252 | axes[3].set_ylabel("Force (N)") 253 | 254 | for ax in axes: 255 | ax.grid(True) 256 | ax.legend() 257 | -------------------------------------------------------------------------------- /paper/fig/BEST_GLIS_PC.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/paper/fig/BEST_GLIS_PC.pdf -------------------------------------------------------------------------------- /paper/fig/BEST_GLIS_PI.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/paper/fig/BEST_GLIS_PI.pdf -------------------------------------------------------------------------------- /paper/fig/ITER_GLIS_PC.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/paper/fig/ITER_GLIS_PC.pdf -------------------------------------------------------------------------------- /paper/fig/ITER_GLIS_PC_500.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/paper/fig/ITER_GLIS_PC_500.pdf -------------------------------------------------------------------------------- /paper/fig/ITER_GLIS_PI_500.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/paper/fig/ITER_GLIS_PI_500.pdf -------------------------------------------------------------------------------- /paper/fig/MPC_CPUTIME_GLIS_PI.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/paper/fig/MPC_CPUTIME_GLIS_PI.pdf -------------------------------------------------------------------------------- /paper/fig/cart_pole.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/paper/fig/cart_pole.pdf -------------------------------------------------------------------------------- /paper/fig/sym/F.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /paper/fig/sym/elle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /paper/fig/sym/elleM.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /paper/fig/sym/m (Case Conflict).svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /paper/fig/sym/m.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /paper/fig/sym/p.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /paper/fig/sym/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /paper/ms.bib: -------------------------------------------------------------------------------- 1 | @article{Jon09, 2 | title={{DIRECT} global optimization algorithm}, 3 | author={D.R. Jones}, 4 | journal={Encyclopedia of Optimization}, 5 | pages={725--735}, 6 | year={2009}, 7 | } 8 | 9 | @article{HN99, 10 | title={Global optimization by multilevel coordinate search}, 11 | author={W. Huyer and A. Neumaier}, 12 | journal={Journal of Global Optimization}, 13 | volume={14}, 14 | number={4}, 15 | pages={331--355}, 16 | year={1999}, 17 | } 18 | 19 | 20 | @incollection{Han06, 21 | title={The {CMA} evolution strategy: a comparing review}, 22 | author={N. Hansen}, 23 | booktitle={Towards a new evolutionary computation}, 24 | pages={75--102}, 25 | year={2006}, 26 | } 27 | 28 | @inproceedings{EK95, 29 | title={A new optimizer using particle swarm theory}, 30 | author={R. Eberhart and J. Kennedy}, 31 | booktitle={Proceedings of the Sixth International Symposium on Micro Machine and Human Science}, 32 | pages={39--43}, 33 | year={1995}, 34 | address={Nagoya}, 35 | } 36 | 37 | @article{VV09, 38 | author={A.I.F. Vaz and L.N. Vicente}, 39 | title={{PSwarm}: A hybrid solver for linearly constrained global derivative-free optimization}, 40 | journal={Optimization Methods and Software}, 41 | volume=24, 42 | year=2009, 43 | pages={669--685}, 44 | note={\url{http://www.norg.uminho.pt/aivaz/pswarm/}}, 45 | } 46 | 47 | @article{Ken10, 48 | title={Particle swarm optimization}, 49 | author={J. Kennedy}, 50 | journal={Encyclopedia of Machine Learning}, 51 | pages={760--766}, 52 | year={2010}, 53 | } 54 | 55 | @article{SWMW89, 56 | title={Design and analysis of computer experiments}, 57 | author={J. Sacks and W.J. Welch and T.J. Mitchell and H.P. Wynn}, 58 | journal={Statistical Science}, 59 | pages={409--423}, 60 | year={1989}, 61 | } 62 | 63 | @article{CB17, 64 | author ={G. Cimini and A. Bemporad}, 65 | title ={Exact Complexity Certification of Active-Set 66 | Methods for Quadratic Programming}, 67 | journal =ieeetac, 68 | year = 2017, 69 | volume =62, 70 | number=12, 71 | pages = {6094--6109}, 72 | } 73 | @article{bemporad2002explicit, 74 | title={The explicit linear quadratic regulator for constrained systems}, 75 | author={Bemporad, Alberto and Morari, Manfred and Dua, Vivek and Pistikopoulos, Efstratios N}, 76 | journal={Automatica}, 77 | volume={38}, 78 | number={1}, 79 | pages={3--20}, 80 | year={2002}, 81 | publisher={Elsevier} 82 | } 83 | 84 | @inproceedings{bemporad2006model, 85 | title={Model predictive control design: New trends and tools}, 86 | author={Bemporad, Alberto}, 87 | booktitle={Proceedings of the 45th IEEE Conference on Decision and Control}, 88 | pages={6678--6683}, 89 | year={2006} 90 | } 91 | 92 | @book{borrelli2017predictive, 93 | title={Predictive control for linear and hybrid systems}, 94 | author={Borrelli, Francesco and Bemporad, Alberto and Morari, Manfred}, 95 | year={2017}, 96 | publisher={Cambridge University Press} 97 | } 98 | 99 | 100 | @article{FoPiToSa2016, 101 | year = {2016}, 102 | author = {Formentin, S. and Piga, D. and T\'oth, R. and Saveresi, S. M.}, 103 | title = {Direct learning of {LPV} controllers from data}, 104 | journal = {Automatica}, 105 | volume = {65}, 106 | pages = {98--110}, 107 | } 108 | 109 | @article{PiFoBe2017, 110 | author = {Piga, D. and Formentin, S. and Bemporad, A.}, 111 | title = {Direct data-driven control of constrained systems}, 112 | journal = {IEEE Transactions on Control Systems Technology}, 113 | volume={25}, 114 | number={4}, 115 | pages={331--351}, 116 | year={2018}, 117 | } 118 | 119 | @article{shahriari2016taking, 120 | title={Taking the human out of the loop: A review of bayesian optimization}, 121 | author={Shahriari, Bobak and Swersky, Kevin and Wang, Ziyu and Adams, Ryan P and De Freitas, Nando}, 122 | journal={Proceedings of the IEEE}, 123 | volume={104}, 124 | number={1}, 125 | pages={148--175}, 126 | year={2016}, 127 | publisher={IEEE} 128 | } 129 | 130 | @article{brochu2010tutorial, 131 | title={A tutorial on {Bayesian} optimization of expensive cost functions, with application to active user modeling and hierarchical reinforcement learning}, 132 | author={Brochu, E. and Cora, V. M. and {De Freitas}, N.}, 133 | journal={arXiv preprint arXiv:1012.2599}, 134 | year={2010} 135 | } 136 | 137 | 138 | @inproceedings{bansal2017goal, 139 | title={Goal-driven dynamics learning via bayesian optimization}, 140 | author={Bansal, Somil and Calandra, Roberto and Xiao, Ted and Levine, Sergey and Tomlin, Claire J}, 141 | booktitle={IEEE 56th Annual Conference on Decision and Control}, 142 | pages={5168--5173}, 143 | year={2017} 144 | } 145 | 146 | @article{piga2019performance, 147 | title={Performance-oriented model learning for data-driven {MPC} design}, 148 | author={Piga, Dario and Forgione, Marco and Formentin, Simone and Bemporad, Alberto}, 149 | journal={IEEE Control Systems Letters}, 150 | volume={3}, 151 | number={3}, 152 | pages={577--582}, 153 | year={2019}, 154 | publisher={IEEE} 155 | } 156 | 157 | @inproceedings{stellato2018osqp, 158 | title={{OSQP}: An operator splitting solver for quadratic programs}, 159 | author={Stellato, Bartolomeo and Banjac, Goran and Goulart, Paul and Bemporad, Alberto and Boyd, Stephen}, 160 | booktitle={2018 UKACC 12th International Conference on Control (CONTROL)}, 161 | note={\url{https://osqp.org}}, 162 | pages={339--339}, 163 | year={2018} 164 | } 165 | 166 | @article{bemporad2019global, 167 | title={Global optimization via inverse distance weighting}, 168 | author={Bemporad, Alberto}, 169 | journal={arXiv preprint arXiv:1906.06498}, 170 | note={\url{http://cse.lab.imtlucca.it/~bemporad/glis/}}, 171 | year={2019} 172 | } 173 | 174 | @article{bombois2006least, 175 | title={Least costly identification experiment for control}, 176 | author={Bombois, Xavier and Scorletti, G{\'e}rard and Gevers, Michel and Van den Hof, Paul MJ and Hildebrand, Roland}, 177 | journal={Automatica}, 178 | volume={42}, 179 | number={10}, 180 | pages={1651--1662}, 181 | year={2006}, 182 | publisher={Elsevier} 183 | } 184 | 185 | 186 | @inproceedings{roveda2019control, 187 | title={Control Parameters Tuning Based on Bayesian Optimization for Robot Trajectory Tracking}, 188 | author={Roveda, Loris and Forgione, Marco and Piga, Dario}, 189 | booktitle={2019 Workshop on Learning for Industry 4.0 (ICRA)}, 190 | pages={902--907}, 191 | year={2019} 192 | } 193 | 194 | @inproceedings{driess2017constrained, 195 | title={Constrained bayesian optimization of combined interaction force/task space controllers for manipulations}, 196 | author={Drie{\ss}, Danny and Englert, Peter and Toussaint, Marc}, 197 | booktitle={2017 IEEE International Conference on Robotics and Automation (ICRA)}, 198 | pages={902--907}, 199 | year={2017} 200 | } 201 | 202 | @inproceedings{koller2018learning, 203 | title={Learning-based Model Predictive Control for Safe Exploration}, 204 | author={Koller, Torsten and Berkenkamp, Felix and Turchetta, Matteo and Krause, Andreas}, 205 | booktitle={2018 IEEE Conference on Decision and Control (CDC)}, 206 | pages={6059--6066}, 207 | year={2018} 208 | } 209 | 210 | @article{mckay1979comparison, 211 | title={Comparison of three methods for selecting values of input variables in the analysis of output from a computer code}, 212 | author={McKay, Michael D and Beckman, Richard J and Conover, William J}, 213 | journal={Technometrics}, 214 | volume={21}, 215 | number={2}, 216 | pages={239--245}, 217 | year={1979}, 218 | publisher={Taylor \& Francis} 219 | } 220 | 221 | @inproceedings{forgione2020efficient, 222 | title={{E}fficient {C}alibration of {E}mbedded {MPC}}, 223 | author={Forgione, Marco and Piga, Dario and Bemporad, Alberto}, 224 | booktitle={Proc. of the 21st IFAC World Congress, Berlin, Germany}, 225 | year={2020} 226 | } 227 | -------------------------------------------------------------------------------- /paper/ms.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/paper/ms.pdf -------------------------------------------------------------------------------- /paper/ms.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | \usepackage{graphicx} % include this line if your document contains figures 3 | \usepackage[authoryear]{natbib} 4 | \bibliographystyle{abbrvnat} 5 | \setcitestyle{authoryear} 6 | %\usepackage{color} 7 | %\usepackage{algorithm} 8 | %\usepackage{multirow} 9 | \usepackage{amsmath} % assumes amsmath package installed 10 | \usepackage{amssymb} % assumes amsmath package installed 11 | \usepackage{url} 12 | \usepackage{authblk} 13 | 14 | \usepackage{algorithm} 15 | % Used for Algorithm: 16 | \let\labelindent\relax 17 | \usepackage{enumitem} 18 | \renewcommand{\theenumi}{\arabic{enumi}} 19 | \renewcommand{\labelenumi}{\theenumi.} 20 | \renewcommand{\theenumii}{\arabic{enumii}} 21 | \renewcommand{\labelenumii}{\theenumi.\theenumii.} 22 | \renewcommand{\theenumiii}{\arabic{enumiii}} 23 | \renewcommand{\labelenumiii}{\theenumi.\theenumii.\theenumiii.} 24 | \renewcommand{\theenumiv}{\arabic{enumiv}} 25 | \renewcommand{\labelenumiv}{\theenumi.\theenumii.\theenumiii.\theenumiv.} 26 | 27 | \usepackage{subcaption} 28 | %% New Variables %% 29 | \newcommand{\Texp}{T_{\mathrm{exp}}} 30 | \newcommand{\Tstop}{T_{\mathrm{stop}}} 31 | \newcommand{\TMPC}{T_{\mathrm{s}}^{\mathrm{MPC}}} 32 | \newcommand{\TCALCMPC}{T_{\mathrm{calc}}^{\mathrm{MPC}}} 33 | \newcommand{\Nu}{N_u} 34 | \newcommand{\Nc}{\Nu} 35 | \newcommand{\Np}{N_p} 36 | \newcommand{\Ts}{T_{\mathrm{s}}} 37 | \newcommand{\Tsmin}{T_{\mathrm{s,min}}} 38 | \newcommand{\JC}{{J^\mathrm{cl}}} 39 | \newcommand{\JCt}{{\tilde{J}^\mathrm{cl}}} 40 | \newcommand{\JCh}{{\hat{J}^\mathrm{cl}}} 41 | \newcommand{\diag}{\mathrm{diag}} 42 | \newcommand{\Nin}{n_{\mathrm{in}}} 43 | \newcommand{\ntheta}{n_\theta} 44 | 45 | \newcommand{\nin}{n_u} 46 | \newcommand{\nstate}{n_x} 47 | \newcommand{\nout}{n_y} 48 | 49 | \newtheorem{remark}{Remark}{\normalfont}{\normalfont} 50 | 51 | % For internal comments 52 | \usepackage{color} 53 | \newcommand{\DP}[1]{\textbf{\color{red}{[DP: #1]}}} 54 | \newcommand{\AB}[1]{\textbf{\color{green}{[AB: #1]}}} 55 | \newcommand{\MF}[1]{\textbf{\color{blue}{[MF: #1]}}} 56 | 57 | \title{Efficient Calibration of Embedded MPC} 58 | 59 | \author[1]{Marco Forgione} 60 | \author[1]{Dario Piga} 61 | \author[2]{Alberto Bemporad} 62 | \affil[1]{IDSIA Dalle Molle Institute for Artificial Intelligence SUPSI-USI, Manno, Switzerland} 63 | \affil[2]{IMT School for Advanced Studies Lucca, Lucca, Italy} 64 | 65 | 66 | \begin{document} 67 | \maketitle 68 | 69 | 70 | \begin{abstract} % Abstract of not more than 250 words. 71 | Model Predictive Control (MPC) is a powerful and flexible design tool of high-performance controllers for physical systems in 72 | the presence of input and output constraints. 73 | A challenge for the practitioner applying MPC is the need of tuning a large number of parameters such as prediction and control horizons, weight matrices of the MPC cost function, and observer gains, according to different trade-offs. The MPC design task is even more involved when the control law has to be deployed to an embedded hardware unit endowed with limited computational resources. In this case, real-time system requirements limit the complexity of the applicable MPC configuration, engendering additional design tradeoffs and requiring to tune further parameters, such as the sampling time and the tolerances used in the on-line numerical solver. 74 | To take into account closed-loop performance and real-time requirements, in this paper we tackle the embedded MPC design problem using a global, data-driven, optimization approach 75 | We showcase the potential of this approach by tuning an MPC controller on two hardware platforms characterized by largely different computational capabilities. 76 | \end{abstract} 77 | 78 | \vskip 1em 79 | %\begin{large} 80 | \noindent\rule{\textwidth}{1pt} 81 | To cite this work, please use the following bibtex entry: 82 | \begin{verbatim} 83 | @inproceedings{forgione2020efficient, 84 | title={{E}fficient {C}alibration of {E}mbedded {MPC}}, 85 | author={Forgione, Marco and Piga, Dario and Bemporad, Alberto}, 86 | booktitle={Proc. of the 21st IFAC World Congress, Berlin, Germany}, 87 | year={2020} 88 | } 89 | \end{verbatim} 90 | \vskip 1em 91 | Using the plain bibtex style, the bibliographic entry should look like:\\ \\ 92 | M. Forgione, D. Piga, and A. Bemporad. Efficient Calibration of Embedded {MPC}. In \textit{Proc. of the 21st IFAC World Congress, Berlin, Germany}, 2020. 93 | 94 | \noindent\rule{\textwidth}{1pt} 95 | %\end{large} 96 | 97 | 98 | \section{Introduction} 99 | %\citep{forgione2020efficient} % to test the entry 100 | Model Prediction Control (MPC) is an advanced control technology that is getting widely popular in different application 101 | domains \citep{borrelli2017predictive}. 102 | The main technical reason of its success is the ability to optimally coordinate inputs and outputs of multivariable systems in the presence of input/output constraints. 103 | %The main technical reason of its success is the ability to handle multivariable systems in the presence of input and output constraints. 104 | %The main technical reason of its success is the ability to optimally coordinate multiple inputs and outputs in the presence of input and output constraints. 105 | Besides, the intuitive interpretation of MPC as an optimal controller with respect to a given objective function makes it accessible even to practitioners with limited control background. 106 | 107 | Nonetheless, calibrating a high-performance MPC controller taking advantage of all the available tuning knobs may still require substantial effort. The challenges normally encountered are: (i) to choose parameters such as prediction and horizon, cost function weight matrices, and observer gains in order to meet desired closed-loop requirements; (ii) to implement the MPC control law on the target hardware platform, ensuring that all computations are performed in real time. 108 | 109 | Regarding challenge ($i$), the final MPC control law is determined by the prediction model, the specified cost function, and input/output constraints. 110 | %the designer has to specify input/output constraints, a model, and a cost function. 111 | Leaving aside the constraints, which may be considered direct problem specifications, the practitioner has yet to define the plant model for prediction and the cost function. The model is typically obtained from first-principle laws or estimated from measured data. However, when deriving such a model, a tradeoff emerges between accuracy and complexity, and, most of the times, it is difficult to decide \emph{a priori} how accurate the model should be in order to achieve satisfactory closed-loop performance~\citep{FoPiToSa2016,PiFoBe2017}. 112 | %This issue has been recognized as crucial in the Systems and Control community and is studied intensively in the so-called \emph{Identification for Control} (I4C) field. According to the I4C paradigm, the identification procedure should be performed keeping in mind the final control objective and thus capture those model dynamics that most affect the control performance \citep{bombois2006least}. 113 | % 114 | 115 | As for the MPC cost function, it should represent the underlying engineering or economic objective and --- in some cases --- it could also be a direct specification. However, the MPC cost function is often constrained to have a specific structure, typically a quadratic form of predicted input/output values, to allow the use of very efficient quadratic programming (QP) numerical optimization 116 | algorithms. Conversely, the true underlying objective resulting from a combination of time- or frequency-domain specifications (or economical considerations) may be formulated more naturally in a different form than a purely quadratic objective. In these cases, the MPC cost function has to be considered as a tuning knob available to achieve the actual closed-loop goal, rather than an exact quantification of the goal itself. 117 | 118 | Challenge ($ii$) is particularly relevant for fast systems controlled by embedded hardware platforms, such as mobile robots, automotive and aerospace systems, \emph{etc}. Indeed, in embedded systems the on-board computational power is usually limited, \emph{e.g.}, by cost, weight, power consumption, and battery life constraints. Therefore, real-time requirements must be taken into account, further complicating the overall MPC design task. For instance, due to throughput and memory limitations, the MPC sampling time cannot be chosen arbitrarily small and the control horizon cannot be chosen arbitrarily large. 119 | Moreover, the low-level design choices of the MPC implementation become crucial and should be taken into account in the design phase. For instance, the engineer should carefully decide whether to go for an explicit MPC approach where the solution is pre-computed offline for all states in a given range \citep{bemporad2002explicit}, or solve the MPC problem on-line by numerical optimization. The first approach is potentially very fast, but requires storing a large lookup table whose size increases with the MPC problem complexity. The applicability of this method is thus limited by the available system memory. Conversely, the second approach has generally a smaller memory footprint, but requires solving a numerical optimization problem on-line. Thus, in the latter case, the MPC problem complexity and the hardware's computational power limit the maximum controller update frequency. 120 | 121 | %Moreover, when the MPC control law is computed by numerical optimization --- for instance using a QP solver in the case of linear constrained MPC --- the optimizer type and all its hyperparameters may be considered as MPC tuning knobs. 122 | 123 | When the MPC law is computed by numerical optimization (a QP solver in case of linear MPC), the hyperparameters of the optimizer are also MPC tuning knobs, in that they affect solution accuracy and required computation time. 124 | %At this point, new tradeoffs arise: for instance, one could opt for a control law computed either with high accuracy and low updating frequency, or for another one computed with lower accuracy at a higher frequency. 125 | The overall design of a high-performance MPC must therefore take into account simultaneously high-level aspects related to control systems (model, weights, constraints, prediction horizon, sample time, \emph{etc.}) and low-level aspects of numerical optimization (problem size and solver-related hyperparametes). 126 | 127 | In recent years, data-driven approaches for solving complex engineering tuning problems based on derivative-free global optimization are gaining increasing attention \citep{shahriari2016taking}. 128 | The idea behind these approaches is rather intuitive: the user specifies a search space for the design parameters and the optimization algorithm, based on performance data previously observed, sequentially suggests the new configurations to be tested, aiming to optimize a user-given performance index. The procedure is iterated until a configuration achieving satisfactory performance is found or the maximum number of available tests has been reached. 129 | This approach has also been popularized as Design and Analysis of Computer Experiments (DACE)~\citep{SWMW89}. 130 | %Suitable optimization algorithms for this task should be able to apprach an optimal solution with a small number of possibly noisy function evaluations, and should not require derivative information. 131 | Specialized global optimization algorithms for this task such as Bayesian Optimization 132 | (BO) have been proposed \citep{brochu2010tutorial}. The key feature of these algorithms is their 133 | ability to optimize the objective function with a small number of (possibly noisy) evaluations, without relying on derivative information. 134 | %Suitable optimization algorithms should be able to cope with noisy observations, of. Furthermore, minimize evaluations. 135 | %Global gradient-free optimization algorithms requirin. Furthermore, 136 | %these algorithms should be very efficient in terms of number of function evaluations. 137 | Recently, optimization-based tuning has been successfully applied to control system design~\citep{roveda2019control, driess2017constrained} and to choose the MPC prediction model ~\citep{piga2019performance, bansal2017goal}. 138 | 139 | In this paper, we demonstrate the potential of the optimization-based data-driven approach for the joint tuning of high- and low-level MPC parameters in order to optimize a certain closed-loop performance objective, while ensuring that the control law can be computed in real-time on the hardware platform at hand. 140 | %ensure that a closed-loop performance objective is optimized and ($ii$) the control law can be computed in real-time on the hardware at hand. 141 | We apply a derivative-free global optimization algorithm recently developed by one of the authors~\citep{bemporad2019global}, which has been shown to be very efficient in terms of number of function evaluations required to solve the global optimization problem. 142 | We present the results of our MPC tuning procedure for a simulated cart-pole system on two hardware platforms with very different computational capability, namely a high-end x86-64 workstation and a low-performance ARM-based board (specifically, a Raspberry PI 3, Model B). We show that our tuning procedure can find an MPC configuration that squeezes the maximum performance out of the two architectures for the given control task. 143 | 144 | The rest of the paper is organized as follows. The MPC problem formulation and its design parameters are introduced in Section \ref{sec:problem}. Next, the data-driven MPC calibration strategy based on global optimization is described in Section \ref{sec:main} and numerical examples are presented in Section \ref{sec:example}. Finally, conclusions and directions for future research are discussed in Section \ref{sec:conclusions} 145 | 146 | \section{Problem formulation}\label{sec:problem} 147 | 148 | Let us consider the following nonlinear continuous-time multi-input multi-output dynamical system 149 | in state-space form 150 | \begin{subequations} \label{eqn:sys} 151 | \begin{align} 152 | \dot x &= f(x,u) \\ 153 | y &= g(x,u), 154 | \end{align}% 155 | \label{mathcalS}% 156 | \end{subequations} 157 | where $u\in \mathbb{R}^{\nin}$ is the vector of control inputs, $x\in \mathbb{R}^{\nstate}$ 158 | the state vector, $y\in \mathbb{R}^{\nout}$ the controlled outputs, $\dot x$ denotes the time derivative of the state $x$, and $f: \mathbb{R}^{\nstate+\nin} \to\mathbb{R}^{\nstate}$ and $g: \mathbb{R}^{\nstate+\nin} \to\mathbb{R}^{\nout}$ are the state and output mappings, respectively. 159 | Output variables can be collected and used for real-time control at a sampling time $\Ts \geq \Tsmin$, where $\Tsmin$ is the minimum sampling time achievable by the measurement system. 160 | 161 | %signals available at a regular time interval $\Ts$. 162 | We aim at synthesizing an MPC (with a state estimator) for~\eqref{eqn:sys} such that the resulting closed-loop system minimizes a certain closed-loop performance index $\JC$. This performance index is defined as a continuous-time functional $\JC(y_{[0,\;\Texp]}, u_{[0,\;\Texp]})$ in an experiment of fixed duration $\Texp$, where $y_{[0,\;\Texp]}$ (resp. $u_{[0,\;\Texp]}$) denotes the output signal $y(t)$ (resp. input signal $u(t)$) within the time interval $[0,\;\Texp]$. 163 | % 164 | %\begin{equation} 165 | % \JC = \JC(y_{[0\;\Texp]}, u_{[0\;\Texp]}). 166 | %\end{equation} 167 | %\MF{Improve notation for a functional cost on CT variables} 168 | %defined over the duration $T$ of an experiment. 169 | % 170 | As an additional requirement, we must ensure that the control law can be computed in real-time on a given hardware platform. This requirement is translated into the constraint $\TCALCMPC \leq \TMPC$, where $\TMPC$ denotes the MPC sampling time and $\TCALCMPC$ the worst-case time required to compute the MPC control law on the given platform. %In order to satisfy the real-time requirements, we need of course that for the chosen control law $\TCALCMPC < \TMPC$. 171 | %\begin{remark} 172 | It is worth remarking that in general the closed-loop performance index $\JC$ is a nonconvex function of the MPC design parameters, that we will defined later. For the sake of generality, $\JC$ has been denoted above as a continuous-time functional over the duration $\Texp$ of the experiment. Thus, $\JC$ does not necessarily correspond to the cost function minimized on-line by MPC. Indeed, the latter is generally defined as a discrete-time quadratic function over a prediction horizon generally shorter than $\Texp$. 173 | %(\DP{dire che la definiamo dopo}) 174 | %\end{remark} 175 | 176 | %\DP{Introdurre le sezioni sotto} 177 | In the following paragraphs, we define the MPC and state estimator design problem, 178 | along with their tuning parameters. 179 | %In the remainder of this section, we provide details on the MPC controller and the state estimator used in this paper, contextually introducing all their adjustable parameters available for design. 180 | %These free parameters will be tuned in Section \ref{sec:main} 181 | %that will be tuned according to the procedure described in Section \ref{sec:main}. 182 | 183 | \subsection{MPC controller} \label{Sec:outerMPC} 184 | We assume that a continuous-time (possibly parametrized) model $M(\theta^m)$ of~\eqref{mathcalS} is available, where $\theta^m$ represents a vector of adjustable model parameters. 185 | For a given choice of $\TMPC$, $M(\theta^m)$ can be linearized and discretized in time, yielding the discrete-time state-space model 186 | \begin{subequations} 187 | \label{eq:SITO} 188 | \begin{align} 189 | x_{t+1}&=A(\TMPC,\theta^m)x_t+B(\TMPC,\theta^m)u_t \label{eq:MPCsysstate}\\ 190 | y_t&=C(\theta^m)x_t+D(\theta^m)u_t \label{eq:MPCsysout} 191 | \end{align} 192 | \end{subequations} 193 | that is used as prediction model for MPC. 194 | 195 | At each time $t$ that is an integer multiple of the MPC sampling time $\TMPC$, MPC solves the minimization problem 196 | \begin{subequations} \label{eq:MPC} 197 | \begin{align} 198 | & \nonumber \min_{\left\{u_{t+k|t}\right\}_{k=0}^{\Nu-1}, \epsilon} \sum_{k=0}^{\Np-1} \left(y_{t+k|t}-y^{\rm{ref}}_{t+k}\right)^\top Q_y\left(y_{t+k|t}-y^{\rm{ref}}_{t+k}\right) +\\ 199 | & \nonumber \qquad \qquad+\sum_{k=0}^{\Np-1} \left(u_{t+k|t}-u^{\rm ref}_{t+k}\right)^\top Q_u \left(u_{t+k|t}-u^{\rm ref}_{t+k}\right) \!+\!\\ 200 | &\qquad \qquad + \sum_{k=0}^{\Np-1} \Delta u_{t+k|t}^\top Q_{\Delta u} \Delta u_{t+k|t} + Q_\epsilon \epsilon^2 \label{eq:MPCcost}\\%+ Q_{\rK} 201 | & \mathrm{s.t. \ } \text{model equations } \eqref{eq:MPCsysstate},\eqref{eq:MPCsysout} \\ 202 | & \quad y_{\mathrm{min}} \!-\! V_y\epsilon\leq y_{t+k|t} \leq y_{\mathrm{max}}+V_y\epsilon, \ k=1,\ldots,\Np \\ 203 | & \quad u_{\mathrm{min}}\!-\!V_u\epsilon \leq u_{t+k|t} \leq u_{\mathrm{max}}+V_u\epsilon, \ k=1,\ldots,\Np \\ 204 | & \quad \Delta u_{\mathrm{min}} -V_{\Delta u}\epsilon \leq \Delta u_{t+k|t}, \ \ k=1,\ldots,\Np \label{eq:MPC:Ducons1} \\ 205 | & \quad \Delta u_{t+k|t} \leq \Delta u_{\mathrm{max}}+V_{\Delta u}\epsilon, \ \ k=1,\ldots,\Np \label{eq:MPC:Ducons2}\\ %\label{eq:MPC:ucons} \\ 206 | & \quad u_{t+\Nu+j|t}= u_{t+\Nu|t}, \ \ j=1,\ldots, \Np-\Nu, \label{eq:MPCconst} 207 | \end{align} 208 | \end{subequations} 209 | where $\Delta u_{t+k|t} = u_{t+k|t}- u_{t+k-1|t}$; $\Np$ and $\Nu$ are the prediction and control horizon, respectively; $Q_{y}$, $Q_{u}$, and $Q_{\Delta u}$ are positive semidefinite weight matrices specifying the MPC cost function; $u_{\rm ref}$ and $y_{\rm ref}$ are the input and output references, respectively; $Q_{\epsilon}$, $V_y$, $V_u$, $V_{\Delta u}$ are positive constants used to soften the input and output constraints, ensuring that the optimization problem~\eqref{eq:MPC} is always feasible. 210 | An MPC calibrator would typically adjust $\Np,\Nc, Q_y, Q_u, Q_{\Delta u}$ using a mix of experience and trial-and-error until the desired closed-loop goals are achieved, 211 | fixing the remaining parameters to their default value. 212 | Such a process, in particular in the absence of a deep knowledge of MPC, can be very time consuming and therefore costly. 213 | 214 | Several parameterizations may be used to simplify the calibration task. For instance, weight matrices $Q_y$, $Q_u$, and $Q_{\Delta u}$ may be constrained to be diagonal (one of the weights may also be chosen equal to one without loss of generality). %[NOT DONE HERE, OMIT] 215 | For notation convenience, we denote by $\theta^c$ the set of all tuning parameters of the MPC problem introduced above.%\eqref{eq:MPC}. 216 | 217 | The solution of the QP problem \eqref{eq:MPC} is computed through numerical optimization. 218 | We denote by $\theta^s$ the QP solver settings, that we assume can also be adjusted by the calibrator. For instance, important solver parameters are the QP's relative and absolute 219 | feasibility/optimality tolerances for termination. Note that the parameters $\theta^s$ influence both the accuracy of the numerical solution (thus, the performance index $\JC$) and the computation time (thus, $\TCALCMPC$). 220 | %on the one hand the accuracy of the numerical solution and thus of performance $\JC$, on the other hand the time required to compute the solution and thus $\TCALCMPC$. 221 | 222 | \subsection{State estimator} 223 | An estimate of the system state $x_t$ is required to solve the MPC optimization problem~\eqref{eq:MPC}. In this paper, we use a Luenberger observer for state estimation: 224 | \begin{subequations} 225 | \begin{align} 226 | \hat x_{t+1|t} &= A \hat x_{t|t-1} + B u_t + L(y_t - C\hat x_{t|t-1})\\ 227 | \hat y_{t+1|t} &= C \hat x_{t+1|t}, 228 | \end{align} 229 | \end{subequations} 230 | where $\hat x_{t|t-1}$ is the state estimate at time $t$ based on observations up to time $t-1$. 231 | We compute the gain $L$ as the standard stationary Kalman filter gain, based on the (linearized) model \eqref{eq:SITO}, assuming positive definite covariance matrices $W_w$ and $W_v$ for the additive process and measurement noise, respectively. 232 | As for the MPC tuning weights, different parametrizations/structures may be used to 233 | define such covariance matrices. The corresponding parameters are the tuning knobs of the state estimator and are denoted as $\theta^f$. 234 | %Let us introduce parameters $\theta_F$ describing the positive-definite matrices $W_v$ and $W_w$: we have $L = L(\TMPC, \theta_M, \theta_F)$. 235 | %As for the MPC weight matrices, different parametrizations may be used. 236 | %\begin{remark} 237 | %The Kalman filter may run at a sample time different than $\TMPC$. 238 | %This could make sense for a standard Kalman filter whose computational complexity is %way lower than the one of constrained MPC. 239 | %However, we avoid this further complication in this paper. 240 | %\end{remark} 241 | 242 | \section{Performance-driven parameter tuning}\label{sec:main} 243 | 244 | For notation convenience, the design parameters $\theta^m$, $\theta^c$, $\theta^s$, $\theta^f$ introduced in the previous section are collected in the single vector $\theta \in \mathbb{R}^{\ntheta}$. 245 | %Note that the closed-loop performance cost is a function of $\theta$. 246 | 247 | %In this section, we describe the experiment-driven MPC calibration procedure that optimize the closed-loop performance index $\JC(y_{[0,\Texp]},u_{[0,\Texp]}; \theta)$, under the real-time constraint $\TCALCMPC(\theta) \leq \TMPC$. 248 | In this section, we describe how to tune $\theta$ through an experiment-driven approach in order to optimize the closed-loop performance index $\JC(y_{[0,\Texp]},u_{[0,\Texp]}; \theta)$, under the real-time constraint $\TCALCMPC(\theta) \leq \TMPC$. 249 | The overall MPC design task can be formalized as the following constrained global 250 | optimization problem 251 | \begin{subequations} 252 | \label{eq:GOP} 253 | \begin{align} 254 | & \min_{\theta \in \Theta}{\JC(y_{[0,\Texp]},u_{[0,\Texp]}; \theta)} \label{eq:TCALCcst} \\ 255 | & \mathrm{s.t. \ } \TCALCMPC(\theta) \leq \eta \TMPC. \label{eq:TCALCcnst} 256 | \end{align} 257 | \end{subequations} 258 | In~\eqref{eq:TCALCcst}, $\Theta \subseteq \mathbb{R}^{\ntheta}$ is the set of admissible values of the design vector $\theta$. Specifically, in this work, $\Theta$ is a box-shaped region delimited by lower and upper bounds for each individual parameter. 259 | The constant $\eta$, $0<\eta<1$ in~\eqref{eq:TCALCcnst} takes into account that, in a practical implementation, a fraction of the controller's computation time should be left available for other tasks. %Furthermore, it is a safety margin. 260 | 261 | It is important to stress that neither for $\JC(\theta)$ nor for $\TCALCMPC(\theta)$ a closed-form expression is available. 262 | Nevertheless, these functions can be evaluated through real experiments or simulations\footnote{In order to evaluate the worst-case computation time $\TCALCMPC$ for a given parameter $\theta$, the control law should be computed on the target hardware directly. In the absence of a platform emulator, this can be done, for instance, with an \emph{hardware-in-the-loop} setup where the target hardware closes the control loop on a simulated system.}. 263 | In the following, we describe the optimization algorithm used to solve problem \eqref{eq:GOP} based on function evaluations of $\JC$ and $\TCALCMPC$. 264 | One of main strengths of this algorithm is its efficiency in terms of number of required 265 | function evaluations. 266 | %ad 267 | 268 | %The optimization algorithm used in this paper is briefly discussed in the remainder of this section. 269 | 270 | %It is important to stress that neither the performance $\JC$ nor the MPC computation time $\TCALCMPC$ are known functions of $\theta$. 271 | 272 | \subsection{Global optimization for parameter selection} \label{sec:GLIS} 273 | First, the constrained optimization problem~\eqref{eq:GOP} is approximated by the following box-constrained problem 274 | \begin{subequations} \label{eqr:probConst} 275 | \begin{align} 276 | & \min_{\theta \in \Theta}{\JCt(\theta)}, 277 | \end{align} 278 | where $\JCt(\theta)$ is defined as the original cost $\JC( \theta)$ plus a continuous barrier function $\ell: \mathbb{R} \rightarrow \mathbb{R}^+$ penalizing the violation of the constraint $\TCALCMPC(\theta) \leq \eta \TMPC$, \emph{i.e.}, 279 | \begin{align} \label{eqn:Jtilde} 280 | & \JCt(\theta) = \JC( \theta) + \ell\left(\TCALCMPC(\theta) - \eta \TMPC\right). 281 | \end{align} 282 | \end{subequations} 283 | 284 | 285 | We solve the optimization problem~\eqref{eqr:probConst} by using the approach described 286 | in~\citep{bemporad2019global}, called GLIS (GLobal optimization based on Inverse distance weighting and radial basis function Surrogates).%, available for download at \url{http://cse.lab.imtlucca.it/~bemporad/glis}. 287 | 288 | The algorithm first runs $\Nin \geq 1$ closed-loop experiments 289 | for $\Nin$ different values of the vector of controller parameters $\theta_n$, $n=1,\ldots,\Nin$, 290 | generated randomly using Latin Hypercube Sampling (LHS)~\citep{mckay1979comparison} 291 | within $\Theta$, measuring the corresponding performance index $\JCt_n$. 292 | Next, at iteration $n \geq \Nin$ a radial basis function $\hat f_{\rm RBF}$ is fit to the available samples $\{(\theta_1,\JCt_1),\dots, (\theta_n,\JCt_n)\}$. This function $\hat f_{\rm RBF}$ is a surrogate of the (non-quantified) performance index $\JCt$. 293 | Another function $z:\Theta\to\mathbb{R}$, that promotes the exploration of the set $\Theta$ in areas that 294 | have not been sampled yet and where the empirical variance of $\JCt-\hat f_{\rm RBF}$ 295 | is large, is summed to $\hat f_{\rm RBF}$ to define an acquisition function $a:\Theta\to\mathbb{R}$. 296 | The function $a$ (which is very easy to evaluate and even differentiate) is then minimized over $\Theta$ by using a global optimization algorithm, 297 | such as Particle Swarm Optimization (PSO)~\citep{EK95,VV09}, 298 | to get a new configuration $\theta_{n+1}$ 299 | of MPC parameters to test. A new closed-loop experiment is performed with controller parameters $\theta = \theta_{n+1}$ and the performance index $\JCt_{n+1}$ is measured. 300 | The algorithm iterates until a stopping criterion is met or a maximum number $n_{\mathrm{max}}$ of iterations is reached. 301 | 302 | The main advantage of using the global optimization algorithm GLIS described above for solving the calibration problem is twofold. First, it is a derivative-free algorithm, and thus it is particularly convenient since a closed-form expression of the cost $\JCt$ as a function of the design parameters $\theta$ is not available. Second, it allows us to tune the controller parameters $\theta$ with a smaller number of experiments compared to other existing global optimization methods, 303 | such as PSO, DIRECT (DIvide a hyper-RECTangle)~\citep{Jon09}, 304 | Multilevel Coordinate Search (MCS)~\citep{HN99}, Genetic Algorithms (GA)~\citep{Han06}, 305 | and usually even than BO as reported in~\citep{bemporad2019global}. 306 | 307 | % The latter point is crucial, since each evaluation can be costly and time-consuming, as it requires a closed-loop experiment. 308 | 309 | %\subsection{Bayesian optimization for parameter selection} 310 | 311 | \section{Numerical Example} 312 | \label{sec:example} 313 | \begin{figure} 314 | \centering 315 | \includegraphics[width=.4\textwidth]{fig/cart_pole.pdf} 316 | \caption{Schematics of the cart-pole system.} 317 | \label{fig:pendulum_cart} 318 | \end{figure} 319 | 320 | As a case study, we consider the problem of controlling the cart-pole system depicted in Fig.~\ref{fig:pendulum_cart}. 321 | We aim at designing an MPC controller that minimizes a given closed-loop performance index $\JC$, while satisfying the constraint~\eqref{eq:TCALCcnst} on the worst-case MPC execution time $\TCALCMPC$ for real-time implementation. 322 | The MPC law is computed using a custom-made Python library that transforms problem~\eqref{eq:MPC} into a standard QP form, which is subsequently solved using the ADMM-based QP solver OSQP \citep{stellato2018osqp}. 323 | The MPC parameters are tuned via global optimization using the solver GLIS 324 | recalled in Section~\ref{sec:GLIS} and retrieved from \url{http://cse.lab.imtlucca.it/~bemporad/glis}. In particular, package version 1.1 was used with default settings. 325 | 326 | The source code generating the results in this paper can be found at \url{https://github.com/forgi86/efficient-calibration-embedded-MPC}. 327 | A standalone installation version of the MPC library is also available at \url{https://github.com/forgi86/pyMPC} for convenient integration in other projects. 328 | 329 | The MPC tuning is performed for two different hardware platforms with remarkably different speed performance: 330 | \begin{itemize} 331 | \item an x86-64 PC equipped with an Intel i5-7300U 2.60~GHz CPU and 32 GB of RAM; 332 | \item a Raspberry PI 3 rev. B board equipped with a 1.2~GHz ARM Cortex-A53 CPU and 1~GB of RAM. 333 | \end{itemize} 334 | 335 | The Raspberry PI 3 is roughly 10 times slower than the PC in computing the MPC law. This leads to different constraints on the maximum controller complexity and thus on the achievable closed-loop performance. 336 | 337 | \subsection{System description} 338 | The cart-pole dynamics are governed by the following continuous-time differential equations which are used to simulate the behavior of the system: 339 | %\begin{subequations*} \label{eqn:pend} 340 | \begin{align*} 341 | (M+m)\ddot p + mL\ddot\phi \cos\phi - mL \dot \phi ^2 \sin \phi + b\dot p &= F\\ 342 | L \ddot \phi + \ddot p \cos \phi - g \sin\phi + f_\phi\dot \phi &=0 , 343 | \end{align*} 344 | %\end{subequations} 345 | where $\phi\,\mathrm{(rad)}$ is the angle of the pendulum with respect to the upright vertical position, $p\,\mathrm{(m)}$ is the cart position, and $F\,\mathrm{(N)}$ is the input force on the cart. The following values of the physical parameters are used: $M=0.5\,\mathrm{kg}$ (cart mass), $m=0.2\,\mathrm{kg}$ (pendulum mass), $L=0.3\,\mathrm{m}$ (rod length), 346 | $g=9.81\,\mathrm{m/s^{2}}$ (gravitational acceleration), $b=0.1\,\mathrm{N/m/s}$, and $f_\phi=0.1\,\mathrm{m/s}$ (friction terms). 347 | Measurements of $p$ and $\phi$ 348 | are supposed to be collected at a minimum sampling time $\Tsmin=1\,\mathrm{ms}$, and are corrupted by additive zero-mean white Gaussian noise sources with standard deviation $0.02\,\mathrm{m}$ and $0.01\,\mathrm{rad}$, respectively. The input force $F$ is perturbed by an additive zero-mean colored Gaussian noise with standard deviation $0.1\,\mathrm{N}$ and bandwidth $5\,\mathrm{rad/s}$. 349 | The force $F$ is bounded to the interval $[-10,\; 10]\, \mathrm{N}$, which is due to actuator saturation, while the cart position $p$ is limited to the interval $[-1.2,\; 1.2]\,\mathrm{m}$, representing the length of the track where the cart moves. 350 | 351 | The system is initialized at $[p(0) \; \dot{p}(0)\; \phi(0)\;\dot{\phi}(0)] = [0 \; 0\; \frac{\pi}{18}\;0]$. In the MPC calibration experiment, the objective is to track a piecewise linear position reference $p^{\mathrm{ref}}$ for the cart passing through the time-position points $\{(0,0)$, $(5,0)$, $(10,0.8)$, $(20,0.8)$, $(25,0)$, $(30,0)$, $(40,0.8)\}$ over an experiment of duration $\Texp=40$ s, while stabilizing the angle $\phi$ to the upright vertical position, \emph{i.e.,} at $\phi=0$. 352 | %The MPC is configured with fixed constraints $F_{\max}\!=\!-F_{\min}\!=\!10~\mathrm{N}$ and $p_{\max}\!=\!-p_{\min}\!=\!1~\mathrm{m}$ on the input force $F$ and the output position $p$, respectively. 353 | %Other MPC settings are free tunable parameters, as discussed later. 354 | %\footnote{Note that the position constraint of MPC is slightly tighter than the physical limit.} 355 | 356 | 357 | The controller is disabled at time $\Tstop < \Texp$ whenever one of the following early termination condition occurs: 358 | \begin{itemize} 359 | \item pendulum falling ($|\phi| > \frac{\pi}{6})$ 360 | \item cart approaching end of the track ($|p| \geq 1.1~\mathrm{m}$) 361 | \item numerical errors in the MPC law computation 362 | \end{itemize} 363 | Similar conditions may be required to ensure safety in the case of real experiments performed on a physical setup. 364 | In our simulation setting, they are still useful to reduce the computational time as they speed up the test of configurations that are definitely not optimal. 365 | Furthermore, early termination is explicitly penalized in our closed-loop performance index (see next paragraph), and thus provides useful information for MPC calibration to the global optimization algorithm. 366 | 367 | \subsection{Performance index} 368 | The closed-loop performance index $\JCt$ is defined as 369 | \begin{multline} 370 | \label{eq:JCL_example} 371 | \JCt = \ln \left ( \int_{t=0}^{\Texp}10 |p^{\mathrm{ref}}(t) - p(t)| + 30 |\phi(t)|\;dt \right )+ \\ 372 | + \ell\left(\TCALCMPC - \eta \TMPC\right) + \ell'(\Texp - \Tstop), 373 | \end{multline} 374 | where the penalty term $\ell$ for real-time implementation of the control law is 375 | \begin{equation} 376 | \label{eq:penalty} 377 | \ell\!=\! \begin{cases} 378 | \!\ln \left(1+ 10^3\frac{\TCALCMPC\!-\!\eta \TMPC}{\eta \TMPC}\right)&\text{if } \TCALCMPC > \eta \TMPC\\ 379 | 0 &\text{otherwise}, 380 | \end{cases} 381 | \end{equation} 382 | with $\eta = 0.8$. Another term $\ell'$ is used to penalize early termination conditions, as previously discussed: 383 | \begin{equation} 384 | \label{eq:penalty_time} 385 | \ell'\!=\! \begin{cases} 386 | \!\ln \left(1+ 10^3\frac{\Texp\!-\!\Tstop}{\Texp}\right)&\text{if } \Tstop < \Texp\\ 387 | 0 & \text{otherwise}. 388 | \end{cases} 389 | \end{equation} 390 | The integral in~\eqref{eq:JCL_example} is approximated using samples collected at the fastest sampling time $\Tsmin$. 391 | %Conversely, the MPC sample $\TMPC$ is one of the MPC design parameters. 392 | 393 | \subsection{Control design parameters} 394 | We have different MPC design parameters to tune in order to minimize the performance-driven 395 | objective~\eqref{eq:JCL_example}. 396 | 397 | As for the MPC cost function, the positive definite weight matrix $Q_y$ is diagonally parameterized as 398 | $\bigl( \begin{smallmatrix} 399 | q_{y_{11}}&0\\ 0&q_{y_{22}} 400 | \end{smallmatrix} \bigr)$, 401 | where $q_{y_{11}}$, and $q_{y_{22}}$ are design parameters taking real values in the interval $[10^{-16},\ 1]$. Since in the example $n_u=1$, the weights $Q_u$ and $Q_{\Delta u}$ are scalars. $Q_{\Delta u}$ is taken as a decision variable and bounded in the range $[10^{-16} ,\ 1]$, while $Q_u$ is set to 0. The prediction horizon $\Np$ is an integer parameter in the range $[5,\ 300]$, while the control horizon $\Nc$ is a fraction $\epsilon_c$ of $\Np$ rounded to the closest integer, with design parameter $\epsilon_c$ restricted to the range $[0.3,\ 1]$. Lastly, the MPC sampling time $\TMPC$ is a design parameter restricted to the range $[1,\ 50]\,\mathrm{ms}$. 402 | 403 | In the QP solver, the relative and absolute feasibility/optimality tolerances are tuned. Specifically, the log of two tolerances are parameters in the range $[-7,\ -1]$. 404 | 405 | As for the state estimator, the 4x4 process noise covariance matrix $W_w$ and the 2x2 output noise covariance matrix $W_v$ are diagonally parametrized, similarly to $Q_y$. 406 | 407 | The system dynamics are assumed to be known. Therefore, there is no tunable model parameter in our design problem. 408 | 409 | Finally, MPC is configured with fixed constraints $F_{\max}\!=\!-F_{\min}\!=\!10~\mathrm{N}$ and $p_{\max}\!=\!-p_{\min}\!=\!1~\mathrm{m}$ on the input force $F$ and on the output position $p$, respectively, while standard values are used for all other MPC settings in \eqref{eq:MPC} not mentioned here. 410 | %\footnote{Note that the position constraint of MPC is slightly tighter than the physical limit.} 411 | 412 | The design parameter $\theta$ has thus dimension $\ntheta=14$. 413 | %Overall, the design parameter $\theta$ consists of $\ntheta=14$ variables. 414 | 415 | \subsection{Results} 416 | 417 | 418 | \begin{figure} 419 | \centering 420 | \begin{subfigure}[b]{\textwidth} 421 | \includegraphics[width=1\linewidth]{fig/ITER_GLIS_PC_500.pdf} 422 | %\caption{} 423 | %\label{fig:Ng1} 424 | \end{subfigure} 425 | 426 | \begin{subfigure}[b]{\textwidth} 427 | \includegraphics[width=1\linewidth]{fig/ITER_GLIS_PI_500.pdf} 428 | %\caption{} 429 | %\label{fig:Ng2} 430 | \end{subfigure} 431 | \caption{Performance cost $\JCt$ vs. iteration index $n$ of GLIS for experiments run on the PC (top) and on the Raspberry PI (bottom).} 432 | \label{fig:iterations} 433 | \end{figure} 434 | 435 | %\end{figure} 436 | 437 | \begin{figure} 438 | \centering 439 | \begin{subfigure}[b]{\textwidth} 440 | \includegraphics[width=1\linewidth]{fig/BEST_GLIS_PC.pdf} 441 | %\caption{} 442 | %\label{fig:Ng1} 443 | \end{subfigure} 444 | 445 | \begin{subfigure}[b]{\textwidth} 446 | \includegraphics[width=1\linewidth]{fig/BEST_GLIS_PI.pdf} 447 | %\caption{} 448 | %\label{fig:Ng2} 449 | \end{subfigure} 450 | \caption{Performance achieved by the designed MPC for experiments run on the PC (top panel) and on the Raspberry PI (bottom panel).} 451 | \label{fig:timetraces} 452 | \end{figure} 453 | 454 | The global optimizer GLIS is run for $n_{\mathrm{max}} = 500$ iterations. The performance cost $\JCt$~\eqref{eq:JCL_example} \emph{vs.} the iteration index $n$ is shown in Fig.~\ref{fig:iterations} for the PC (top) and the Raspberry PI 3 (bottom). It can be observed that, after about $100$ iterations, the majority of the controller parameter configurations proposed by the optimization algorithm are concentrated in regions with low cost $\JCt$. 455 | This is more evident on the Raspberry PI 3 platform, where the set of parameters satisfying the real-time constraint~\eqref{eq:TCALCcnst} is smaller. 456 | Persistent high values $\JCt$ are due (correctly) to the exploration of the parameter space $\Theta$ promoted by the GLIS algorithm. 457 | 458 | 459 | The obtained optimal performance index $\JCt$ after 500 iterations is slightly better on the PC (-0.44) than on the Raspberry PI (0.02), as expected. Indeed, certain MPC configurations characterized, \emph{e.g.}, by small sampling time and long prediction horizon may be feasible on the PC, but not on the Raspberry PI. 460 | 461 | Fig.~\ref{fig:timetraces} shows the time trajectories of position $p$, angle $\phi$, and force $F$ for the optimal MPC controller on the PC (top panel) and on the Raspberry PI (bottom panel), over a validation reference trajectory different from the one used for calibration. A slightly better performance for the MPC implementation on the PC can be appreciated both in terms of a tighter cart position tracking and a lower variance in the angle signal. %throughout the whole experiment. 462 | 463 | Analyzing the two final MPC designs, we noticed that on the PC we have $\TMPC= 6~\mathrm{ms}$, while on the Raspberry PI we have $\TMPC= 22~\mathrm{ms}$. The optimal solution found for the PC platform allows a faster loop update, and thus achieves superior trajectory tracking and noise rejection capabilities. 464 | On the other hand, a larger MPC time is required on the Raspberry PI to guarantee real-time implementation. %, whose violation would otherwise result in a large cost due to the penalty term $\ell(\TCALCMPC)$ in $\JCt$. 465 | % Conversely, the penalty term $\ell(\TCALCMPC)$ is zero for most of the MPC configuration in the parameter space on the PC, where the control law can be computed much faster. Thus, on the PC, a larger portion of the MPC design parameter space is feasible. 466 | 467 | 468 | 469 | \section{Conclusions and follow-up} 470 | \label{sec:conclusions} 471 | We have presented an automated method to calibrate MPC parameters with a limited number of experiments. Real-time implementation constraints are explicitly taken into account in the design in order to allow embedded implementation of the resulting controller. 472 | 473 | Future research will be devoted to find a parameterized solution of the optimal MPC tuning knobs with respect to the reference trajectories, and to the analysis of the generalization properties of the designed controllers against control objectives not considered in the calibration phase. 474 | 475 | 476 | %\begin{ack} 477 | %Place acknowledgments here. 478 | %\end{ack} 479 | 480 | \bibliography{ms} % bib file to produce the bibliography 481 | % with bibtex (preferred) 482 | 483 | \end{document} 484 | -------------------------------------------------------------------------------- /pendulum_MPC_sim.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.sparse as sparse 3 | from pyMPC.mpc import MPCController 4 | from kalman import kalman_design_simple, LinearStateEstimator 5 | from pendulum_model import * 6 | from scipy.integrate import ode 7 | from scipy.interpolate import interp1d 8 | import time 9 | import control 10 | import control.matlab 11 | import numpy.random 12 | 13 | Ts_fast = 1e-3 14 | 15 | Ac_def = np.array([[0, 1, 0, 0], 16 | [0, -b / M, -(g * m) / M, (ftheta * m) / M], 17 | [0, 0, 0, 1], 18 | [0, b / (M * l), (M * g + g * m) / (M * l), -(M * ftheta + ftheta * m) / (M * l)]]) 19 | 20 | Bc_def = np.array([ 21 | [0.0], 22 | [1.0 / M], 23 | [0.0], 24 | [-1 / (M * l)] 25 | ]) 26 | 27 | # Reference input and states 28 | #t_ref_vec = np.array([0.0, 10.0, 20.0, 30.0, 40.0]) 29 | #p_ref_vec = np.array([0.0, 0.8, 0.8, 0.0, 0.0]) 30 | #rp_fun = interp1d(t_ref_vec, p_ref_vec, kind='zero') 31 | t_ref_vec = np.array([0.0, 5.0, 10.0, 20.0, 25.0, 30.0, 40.0, 100.0]) 32 | p_ref_vec = np.array([0.0, 0.0, 0.8, 0.8, 0.0, 0.0, 0.8, 0.8]) 33 | rp_fun = interp1d(t_ref_vec, p_ref_vec, kind='linear') 34 | 35 | 36 | def xref_fun_def(t): 37 | return np.array([rp_fun(t), 0.0, 0.0, 0.0]) 38 | 39 | 40 | #Qx_def = 0.9 * sparse.diags([0.1, 0, 0.9, 0]) # Quadratic cost for states x0, x1, ..., x_N-1 41 | #QxN_def = Qx_def 42 | #Qu_def = 0.0 * sparse.eye(1) # Quadratic cost for u0, u1, ...., u_N-1 43 | #QDu_def = 0.01 * sparse.eye(1) # Quadratic cost for Du0, Du1, ...., Du_N-1 44 | 45 | Ts_MPC_def = 10e-3 46 | Qx_def = 1.0 * sparse.diags([1.0, 0, 5.0, 0]) # Quadratic cost for states x0, x1, ..., x_N-1 47 | QxN_def = Qx_def 48 | 49 | Qu_def = 0.0 * sparse.eye(1) # Quadratic cost for u0, u1, ...., u_N-1 50 | QDu_def = 1e-5/(Ts_MPC_def**2) * sparse.eye(1) # Quadratic cost for Du0, Du1, ...., Du_N-1 51 | 52 | DEFAULTS_PENDULUM_MPC = { 53 | 'xref_fun': xref_fun_def, 54 | 'uref': np.array([0.0]), # N 55 | 'std_npos': 0.02, # m 56 | 'std_nphi': 0.01, # rad 57 | 'std_dF': 0.1, # N 58 | 'w_F': 5, # rad/s 59 | 'len_sim': 40, #s 60 | 61 | 'Ac': Ac_def, 62 | 'Bc': Bc_def, 63 | 'Ts_MPC': Ts_MPC_def, 64 | 'Q_kal': np.diag([0.1, 10, 0.1, 10]), 65 | 'R_kal': 1*np.eye(2), 66 | 67 | 'Np': 100, 68 | 'Nc': 50, 69 | 'Qx': Qx_def, 70 | 'QxN': QxN_def, 71 | 'Qu': Qu_def, 72 | 'QDu': QDu_def, 73 | 'QP_eps_abs': 1e-3, 74 | 'QP_eps_rel': 1e-3, 75 | 'seed_val': None 76 | 77 | } 78 | 79 | 80 | def get_parameter(sim_options, par_name): 81 | return sim_options.get(par_name, DEFAULTS_PENDULUM_MPC[par_name]) 82 | 83 | 84 | def get_default_parameters(sim_options): 85 | """ Which parameters are left to default ??""" 86 | default_keys = [key for key in DEFAULTS_PENDULUM_MPC if key not in sim_options] 87 | return default_keys 88 | 89 | 90 | def simulate_pendulum_MPC(sim_options): 91 | 92 | seed_val = get_parameter(sim_options,'seed_val') 93 | if seed_val is not None: 94 | np.random.seed(seed_val) 95 | 96 | Ac = get_parameter(sim_options, 'Ac') 97 | Bc = get_parameter(sim_options, 'Bc') 98 | 99 | Cc = np.array([[1., 0., 0., 0.], 100 | [0., 0., 1., 0.]]) 101 | 102 | Dc = np.zeros((2, 1)) 103 | 104 | [nx, nu] = Bc.shape # number of states and number or inputs 105 | ny = np.shape(Cc)[0] 106 | 107 | Ts_MPC = get_parameter(sim_options, 'Ts_MPC') 108 | ratio_Ts = int(Ts_MPC // Ts_fast) 109 | Ts_MPC = ((Ts_MPC // Ts_fast)) * Ts_fast # make Ts_MPC an integer multiple of Ts_fast 110 | 111 | # Brutal forward euler discretization 112 | Ad = np.eye(nx) + Ac*Ts_MPC 113 | Bd = Bc*Ts_MPC 114 | Cd = Cc 115 | Dd = Dc 116 | 117 | # Standard deviation of the measurement noise on position and angle 118 | 119 | std_npos = get_parameter(sim_options, 'std_npos') 120 | std_nphi = get_parameter(sim_options, 'std_nphi') 121 | 122 | # Force disturbance 123 | std_dF = get_parameter(sim_options, 'std_dF') 124 | 125 | # disturbance power spectrum 126 | w_F = get_parameter(sim_options, 'w_F') # bandwidth of the force disturbance 127 | tau_F = 1 / w_F 128 | Hu = control.TransferFunction([1], [1 / w_F, 1]) 129 | Hu = Hu * Hu 130 | Hud = control.matlab.c2d(Hu, Ts_fast) 131 | N_sim_imp = tau_F / Ts_fast * 20 132 | t_imp = np.arange(N_sim_imp) * Ts_fast 133 | t, y = control.impulse_response(Hud, t_imp) 134 | #y = y[0] 135 | std_tmp = np.sqrt(np.sum(y ** 2)) # np.sqrt(trapz(y**2,t)) 136 | Hu = Hu / (std_tmp) * std_dF 137 | 138 | 139 | N_skip = int(20 * tau_F // Ts_fast) # skip initial samples to get a regime sample of d 140 | t_sim_d = get_parameter(sim_options, 'len_sim') # simulation length (s) 141 | N_sim_d = int(t_sim_d // Ts_fast) 142 | N_sim_d = N_sim_d + N_skip 143 | e = np.random.randn(N_sim_d) 144 | te = np.arange(N_sim_d) * Ts_fast 145 | _, d = control.forced_response(Hu, te, e, return_x=False) 146 | d_fast = d[N_skip:] 147 | #td = np.arange(len(d)) * Ts_fast 148 | 149 | 150 | Np = get_parameter(sim_options, 'Np') 151 | Nc = get_parameter(sim_options, 'Nc') 152 | 153 | Qx = get_parameter(sim_options, 'Qx') 154 | QxN = get_parameter(sim_options, 'QxN') 155 | Qu = get_parameter(sim_options, 'Qu') 156 | QDu = get_parameter(sim_options, 'QDu') 157 | 158 | # Reference input and states 159 | xref_fun = get_parameter(sim_options, 'xref_fun') # reference state 160 | xref_fun_v = np.vectorize(xref_fun, signature='()->(n)') 161 | 162 | t0 = 0 163 | xref_MPC = xref_fun(t0) 164 | uref = get_parameter(sim_options, 'uref') 165 | uminus1 = np.array([0.0]) # input at time step negative one - used to penalize the first delta u at time instant 0. Could be the same as uref. 166 | 167 | # Constraints 168 | xmin = np.array([-1.0, -np.inf, -np.inf, -np.inf]) 169 | xmax = np.array([1.0, np.inf, np.inf, np.inf]) 170 | 171 | umin = np.array([-10]) 172 | umax = np.array([10]) 173 | 174 | Dumin = np.array([-100*Ts_MPC]) 175 | Dumax = np.array([100*Ts_MPC]) 176 | 177 | # Initialize simulation system 178 | phi0 = 10*2*np.pi/360 179 | x0 = np.array([0, 0, phi0, 0]) # initial state 180 | system_dyn = ode(f_ODE_wrapped).set_integrator('vode', method='bdf') # dopri5 181 | # system_dyn = ode(f_ODE_wrapped).set_integrator('dopri5') 182 | system_dyn.set_initial_value(x0, t0) 183 | system_dyn.set_f_params(0.0) 184 | 185 | QP_eps_rel = get_parameter(sim_options, 'QP_eps_rel') 186 | QP_eps_abs = get_parameter(sim_options, 'QP_eps_abs') 187 | 188 | # Emergency exit conditions 189 | 190 | EMERGENCY_STOP = False 191 | EMERGENCY_POS = 1.1 192 | EMERGENCY_ANGLE = 30*DEG_TO_RAD 193 | 194 | K = MPCController(Ad,Bd,Np=Np,Nc=Nc,x0=x0,xref=xref_MPC,uminus1=uminus1, 195 | Qx=Qx, QxN=QxN, Qu=Qu,QDu=QDu, 196 | xmin=xmin,xmax=xmax,umin=umin,umax=umax,Dumin=Dumin,Dumax=Dumax, 197 | eps_feas = 1e3, eps_rel= QP_eps_rel, eps_abs=QP_eps_abs) 198 | 199 | try: 200 | K.setup(solve=True) # setup initial problem and also solve it 201 | except: 202 | EMERGENCY_STOP = True 203 | 204 | if not EMERGENCY_STOP: 205 | if K.res.info.status != 'solved': 206 | EMERGENCY_STOP = True 207 | 208 | # Basic Kalman filter design 209 | Q_kal = get_parameter(sim_options, 'Q_kal') 210 | R_kal = get_parameter(sim_options, 'R_kal') 211 | try: 212 | L, P, W = kalman_design_simple(Ad, Bd, Cd, Dd, Q_kal, R_kal, type='predictor') 213 | except: 214 | EMERGENCY_STOP = True 215 | 216 | x0_est = x0 217 | KF = LinearStateEstimator(x0_est, Ad, Bd, Cd, Dd, L) 218 | 219 | # Simulate in closed loop 220 | len_sim = get_parameter(sim_options, 'len_sim') # simulation length (s) 221 | nsim = int(np.ceil(len_sim / Ts_MPC)) # simulation length(timesteps) # watch out! +1 added, is it correct? 222 | t_vec = np.zeros((nsim, 1)) 223 | t_calc_vec = np.zeros((nsim,1)) # computational time to get MPC solution (+ estimator) 224 | status_vec = np.zeros((nsim,1)) 225 | x_vec = np.zeros((nsim, nx)) 226 | x_ref_vec = np.zeros((nsim, nx)) 227 | y_vec = np.zeros((nsim, ny)) 228 | y_meas_vec = np.zeros((nsim, ny)) 229 | y_est_vec = np.zeros((nsim, ny)) 230 | x_est_vec = np.zeros((nsim, nx)) 231 | u_vec = np.zeros((nsim, nu)) 232 | x_MPC_pred = np.zeros((nsim, Np+1, nx)) # on-line predictions from the Kalman Filter 233 | 234 | nsim_fast = int(len_sim // Ts_fast) 235 | t_vec_fast = np.zeros((nsim_fast, 1)) 236 | x_vec_fast = np.zeros((nsim_fast, nx)) # finer integration grid for performance evaluation 237 | x_ref_vec_fast = np.zeros((nsim_fast, nx)) # finer integration grid for performance evaluatio 238 | u_vec_fast = np.zeros((nsim_fast, nu)) # finer integration grid for performance evaluatio 239 | Fd_vec_fast = np.zeros((nsim_fast, nu)) # 240 | t_int_vec_fast = np.zeros((nsim_fast, 1)) 241 | emergency_vec_fast = np.zeros((nsim_fast, 1)) # 242 | 243 | t_pred_all = t0 + np.arange(nsim + Np + 1) * Ts_MPC 244 | Xref_MPC_all = xref_fun_v(t_pred_all) 245 | 246 | t_step = t0 247 | x_step = x0 248 | u_MPC = None 249 | for idx_fast in range(nsim_fast): 250 | 251 | ## Determine step type: fast simulation only or MPC step 252 | idx_MPC = idx_fast // ratio_Ts 253 | run_MPC = (idx_fast % ratio_Ts) == 0 254 | 255 | # Output for step i 256 | # Ts_MPC outputs 257 | if run_MPC: # it is also a step of the simulation at rate Ts_MPC 258 | t_vec[idx_MPC, :] = t_step 259 | x_vec[idx_MPC, :] = x_step#system_dyn.y 260 | xref_MPC = xref_fun(t_step) # reference state 261 | t_pred = t_step + np.arange(Np + 1) * Ts_MPC 262 | x_ref_vec[idx_MPC,:] = xref_MPC 263 | 264 | if not EMERGENCY_STOP: 265 | # u_MPC, info_MPC = K.output(return_x_seq=True, return_status=True) # u[i] = k(\hat x[i]) possibly computed at time instant -1 266 | u_MPC, info_MPC = K.output(return_status=True) # u[i] = k(\hat x[i]) possibly computed at time instant -1 267 | else: 268 | u_MPC = np.zeros(nu) 269 | 270 | if not EMERGENCY_STOP: 271 | if info_MPC['status'] != 'solved': 272 | EMERGENCY_STOP = True 273 | 274 | if not EMERGENCY_STOP: 275 | pass 276 | #x_MPC_pred[idx_MPC, :, :] = info_MPC['x_seq'] # x_MPC_pred[i,i+1,...| possibly computed at time instant -1] 277 | u_vec[idx_MPC, :] = u_MPC 278 | 279 | y_step = Cd.dot(x_step) # y[i] from the system 280 | ymeas_step = np.copy(y_step) 281 | ymeas_step[0] += std_npos * np.random.randn() 282 | ymeas_step[1] += std_nphi * np.random.randn() 283 | y_vec[idx_MPC,:] = y_step 284 | y_meas_vec[idx_MPC,:] = ymeas_step 285 | if not EMERGENCY_STOP: 286 | status_vec[idx_MPC,:] = (info_MPC['status'] != 'solved') 287 | 288 | if np.abs(ymeas_step[0]) > EMERGENCY_POS or np.abs(ymeas_step[1]) > EMERGENCY_ANGLE: 289 | EMERGENCY_STOP = True 290 | 291 | 292 | # Ts_fast outputs 293 | t_vec_fast[idx_fast,:] = t_step 294 | x_vec_fast[idx_fast, :] = x_step #system_dyn.y 295 | x_ref_vec_fast[idx_fast, :] = xref_MPC 296 | u_fast = u_MPC + d_fast[idx_fast] 297 | u_vec_fast[idx_fast,:] = u_fast 298 | Fd_vec_fast[idx_fast,:] = d_fast[idx_fast] 299 | emergency_vec_fast[idx_fast,:] = EMERGENCY_STOP 300 | 301 | ## Update to step i+1 302 | 303 | # Controller simulation step at rate Ts_MPC 304 | if run_MPC: 305 | time_calc_start = time.perf_counter() 306 | # Kalman filter: update and predict 307 | #KF.update(ymeas_step) # \hat x[i|i] 308 | #KF.predict(u_MPC) # \hat x[i+1|i] 309 | KF.predict_update(u_MPC, ymeas_step) 310 | # MPC update 311 | if not EMERGENCY_STOP: 312 | Xref_MPC = Xref_MPC_all[idx_MPC:idx_MPC + Np + 1] 313 | K.update(KF.x, u_MPC, xref=Xref_MPC) # update with measurement and reference 314 | #K.update(KF.x, u_MPC, xref=xref_MPC) # update with measurement and reference 315 | t_calc_vec[idx_MPC,:] = time.perf_counter() - time_calc_start 316 | if t_calc_vec[idx_MPC,:] > 2 * Ts_MPC: 317 | EMERGENCY_STOP = True 318 | 319 | # System simulation step at rate Ts_fast 320 | time_integrate_start = time.perf_counter() 321 | system_dyn.set_f_params(u_fast) 322 | system_dyn.integrate(t_step + Ts_fast) 323 | x_step = system_dyn.y 324 | #x_step = x_step + f_ODE_jit(t_step, x_step, u_fast)*Ts_fast 325 | #x_step = x_step + f_ODE(0.0, x_step, u_fast) * Ts_fast 326 | t_int_vec_fast[idx_fast,:] = time.perf_counter() - time_integrate_start 327 | 328 | # Time update 329 | t_step += Ts_fast 330 | 331 | simout = {'t': t_vec, 'x': x_vec, 'u': u_vec, 'y': y_vec, 'y_meas': y_meas_vec, 'x_ref': x_ref_vec, 'x_MPC_pred': x_MPC_pred, 'status': status_vec, 'Fd_fast': Fd_vec_fast, 332 | 't_fast': t_vec_fast, 'x_fast': x_vec_fast, 'x_ref_fast': x_ref_vec_fast, 'u_fast': u_vec_fast, 'emergency_fast': emergency_vec_fast, 333 | 'KF': KF, 'K': K, 'nsim': nsim, 'Ts_MPC': Ts_MPC, 't_calc': t_calc_vec, 334 | 't_int_fast': t_int_vec_fast 335 | } 336 | 337 | return simout 338 | 339 | 340 | if __name__ == '__main__': 341 | 342 | import matplotlib.pyplot as plt 343 | import matplotlib 344 | 345 | 346 | plt.close('all') 347 | 348 | simopt = DEFAULTS_PENDULUM_MPC 349 | 350 | time_sim_start = time.perf_counter() 351 | simout = simulate_pendulum_MPC(simopt) 352 | time_sim = time.perf_counter() - time_sim_start 353 | 354 | t = simout['t'] 355 | x = simout['x'] 356 | u = simout['u'] 357 | y = simout['y'] 358 | y_meas = simout['y_meas'] 359 | x_ref = simout['x_ref'] 360 | x_MPC_pred = simout['x_MPC_pred'] 361 | x_fast = simout['x_fast'] 362 | u_fast = simout['u_fast'] 363 | 364 | t_fast = simout['t_fast'] 365 | x_ref_fast = simout['x_ref_fast'] 366 | Fd_fast = simout['Fd_fast'] 367 | KF = simout['KF'] 368 | status = simout['status'] 369 | 370 | uref = get_parameter(simopt, 'uref') 371 | Np = get_parameter(simopt, 'Np') 372 | nsim = len(t) 373 | nx = x.shape[1] 374 | ny = y.shape[1] 375 | 376 | y_ref = x_ref[:, [0, 2]] 377 | y_OL_pred = np.zeros((nsim-Np-1, Np+1, ny)) # on-line predictions from the Kalman Filter 378 | y_MPC_pred = x_MPC_pred[:, :, [0, 2]] # how to vectorize C * x_MPC_pred?? 379 | y_MPC_err = np.zeros(np.shape(y_OL_pred)) 380 | y_OL_err = np.zeros(np.shape(y_OL_pred)) 381 | for i in range(nsim-Np-1): 382 | u_init = u[i:i+Np+1, :] 383 | x_init = x[i,:] 384 | y_OL_pred[i,:,:] = KF.sim(u_init,x_init) 385 | y_OL_err[i, :, :] = y_OL_pred[i, :, :] - y_meas[i:i + Np + 1] 386 | y_MPC_err[i, :, :] = y_MPC_pred[i, :, :] - y_meas[i:i + Np + 1] 387 | 388 | fig,axes = plt.subplots(3,1, figsize=(10,10)) 389 | axes[0].plot(t, y_meas[:, 0], "b", label='p_meas') 390 | axes[0].plot(t_fast, x_fast[:, 0], "k", label='p') 391 | axes[0].plot(t, y_ref[:,0], "k--", label="p_ref") 392 | idx_pred = 0 393 | axes[0].plot(t[idx_pred:idx_pred+Np+1], y_OL_pred[0, :, 0], 'r', label='Off-line k-step prediction') 394 | axes[0].plot(t[idx_pred:idx_pred+Np+1], y_MPC_pred[0, :, 0], 'c', label='MPC k-step prediction' ) 395 | axes[0].plot(t[idx_pred:idx_pred+Np+1], y_OL_err[0, :, 0], 'r--', label='Off-line prediction error') 396 | axes[0].plot(t[idx_pred:idx_pred+Np+1], y_MPC_err[0, :, 0], 'c--', label='MPC prediction error') 397 | axes[0].set_ylim(-0.2,1.0) 398 | axes[0].set_title("Position (m)") 399 | 400 | 401 | axes[1].plot(t, y_meas[:, 1]*RAD_TO_DEG, "b", label='phi_meas') 402 | axes[1].plot(t_fast, x_fast[:, 2]*RAD_TO_DEG, 'k', label="phi") 403 | idx_pred = 0 404 | axes[1].plot(t[idx_pred:idx_pred+Np+1], y_OL_pred[0, :, 1]*RAD_TO_DEG, 'r', label='Off-line k-step prediction') 405 | axes[1].plot(t[idx_pred:idx_pred+Np+1], y_MPC_pred[0, :, 1]*RAD_TO_DEG, 'c',label='MPC k-step prediction' ) 406 | axes[1].plot(t[idx_pred:idx_pred+Np+1], y_OL_err[0, :, 1]*RAD_TO_DEG, 'r--', label='Off-line prediction error' ) 407 | axes[1].plot(t[idx_pred:idx_pred+Np+1], y_MPC_err[0, :, 1]*RAD_TO_DEG, 'c--', label='MPC prediction error') 408 | axes[1].set_ylim(-20,20) 409 | axes[1].set_title("Angle (deg)") 410 | 411 | axes[2].plot(t, u[:,0], label="u") 412 | axes[2].plot(t, uref*np.ones(np.shape(t)), "r--", label="u_ref") 413 | axes[2].set_ylim(-8,8) 414 | axes[2].set_title("Force (N)") 415 | 416 | for ax in axes: 417 | ax.grid(True) 418 | ax.legend() 419 | 420 | # In[MPC computational time] 421 | t_calc = simout['t_calc'] 422 | fig, ax = plt.subplots(1, 1, figsize=(5, 5)) 423 | ax.hist(t_calc * 1000, bins=100) 424 | ax.set_title('MPC computation time)') 425 | ax.grid(True) 426 | ax.set_xlabel('Time (ms)') 427 | 428 | # In[ODE integration time] 429 | t_int = simout['t_int_fast'] 430 | fig, ax = plt.subplots(1, 1, figsize=(5, 5)) 431 | ax.hist(t_int * 1e3, bins=100) 432 | ax.set_title('ODE integration time)') 433 | ax.grid(True) 434 | ax.set_xlabel('Time (ms)') 435 | 436 | # In[MPC computation time ] 437 | fig, axes = plt.subplots(4, 1, figsize=(14, 10), sharex=True) 438 | axes[0].plot(t, y_meas[:, 0], "b", label='p_meas') 439 | axes[0].plot(t_fast, x_fast[:, 0], "k", label='p') 440 | axes[0].step(t, y_ref[:, 0], "k--", where='post', label="p_ref") 441 | axes[0].set_ylim(-0.2, 1.0) 442 | axes[0].set_xlabel("Simulation time (s)") 443 | axes[0].set_ylabel("Position (m)") 444 | 445 | axes[1].step(t, t_calc[:, 0] * 1e3, "b", where='post', label='T_MPC') 446 | axes[1].set_xlabel("Simulation time (s)") 447 | axes[1].set_ylabel("MPC time (ms)") 448 | axes[1].set_ylim(0, 4.0) 449 | axes[2].step(t_fast[1:], t_int[1:, 0] * 1e3, "b", where='post', label='T_ODE') # why is 1st slow? check numba 450 | axes[2].set_xlabel("Simulation time (s)") 451 | axes[2].set_ylabel("ODE time (ms)") 452 | axes[2].set_ylim(0, 0.3) 453 | axes[3].step(t, u[:, 0], where='post', label="F") 454 | axes[3].step(t_fast, u_fast[:, 0], where='post', label="F_d") 455 | axes[3].set_xlabel("Simulation time (s)") 456 | axes[3].set_ylabel("Force (N)") 457 | 458 | for ax in axes: 459 | ax.grid(True) 460 | ax.legend() 461 | 462 | time_integrate = np.sum(simout['t_int_fast']) 463 | time_MPC = np.sum(simout['t_calc']) 464 | print(f'Total time: {time_sim:.2f}, Integration time: {time_integrate:.2f}, MPC time:{time_MPC:.2f}') 465 | 466 | Ts_MPC = simout['Ts_MPC'] 467 | -------------------------------------------------------------------------------- /pendulum_model.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # Constants # 4 | M = 0.5 5 | m = 0.2 6 | b = 0.1 7 | ftheta = 0.1 8 | l = 0.3 9 | g = 9.81 10 | RAD_TO_DEG = 360.0/2.0/np.pi 11 | DEG_TO_RAD = 1.0/RAD_TO_DEG 12 | 13 | # Nonlinear dynamics ODE: 14 | # \dot x = f_ODE(x,u) 15 | from numba import jit 16 | 17 | @jit("float64[:](float64,float64[:],float64[:])", nopython=True, cache=True) 18 | def f_ODE_jit(t, x, u): 19 | F = u[0] 20 | v = x[1] 21 | theta = x[2] 22 | omega = x[3] 23 | der = np.zeros(x.shape) 24 | der[0] = v 25 | der[1] = (m * l * np.sin(theta) * omega ** 2 - m * g * np.sin(theta) * np.cos(theta) + m * ftheta * np.cos( 26 | theta) * omega + F - b * v) / (M + m * (1 - np.cos(theta) ** 2)) 27 | der[2] = omega 28 | der[3] = ((M + m) * (g * np.sin(theta) - ftheta * omega) - m * l * omega ** 2 * np.sin(theta) * np.cos( 29 | theta) - ( 30 | F - b * v) * np.cos(theta)) / (l * (M + m * (1 - np.cos(theta) ** 2))) 31 | return der 32 | 33 | 34 | def f_ODE_wrapped(t,x,u): 35 | return f_ODE_jit(t,x,u) 36 | 37 | def f_ODE(t, x, u): 38 | F = u 39 | v = x[1] 40 | theta = x[2] 41 | omega = x[3] 42 | der = np.zeros(x.shape) 43 | der[0] = v 44 | der[1] = (m * l * np.sin(theta) * omega ** 2 - m * g * np.sin(theta) * np.cos(theta) + m * ftheta * np.cos( 45 | theta) * omega + F - b * v) / (M + m * (1 - np.cos(theta) ** 2)) 46 | der[2] = omega 47 | der[3] = ((M + m) * (g * np.sin(theta) - ftheta * omega) - m * l * omega ** 2 * np.sin(theta) * np.cos( 48 | theta) - ( 49 | F - b * v) * np.cos(theta)) / (l * (M + m * (1 - np.cos(theta) ** 2))) 50 | return der 51 | 52 | 53 | if __name__ == '__main__': 54 | f_ODE_jit(0.1, np.array([0.3, 0.2, 0.1, 0.4]), 3.1) 55 | -------------------------------------------------------------------------------- /pyMPC/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/pyMPC/__init__.py -------------------------------------------------------------------------------- /pyMPC/mpc.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy as sp 3 | import scipy.sparse as sparse 4 | import osqp 5 | import warnings 6 | 7 | 8 | def __is_vector__(vec): 9 | if vec.ndim == 1: 10 | return True 11 | else: 12 | if vec.ndim == 2: 13 | if vec.shape[0] == 1 or vec.shape[1] == 0: 14 | return True 15 | else: 16 | return False 17 | return False 18 | 19 | 20 | def __is_matrix__(mat): 21 | if mat.ndim == 2: 22 | return True 23 | else: 24 | return False 25 | 26 | class MPCController: 27 | """ This class implements an MPC controller 28 | 29 | Attributes 30 | ---------- 31 | Ad : 2D array_like. Size: (nx, nx) 32 | Discrete-time system matrix Ad. 33 | Bd : 2D array-like. Size: (nx, nu) 34 | Discrete-time system matrix Bd. 35 | Np : int 36 | Prediction horizon. Default value: 20. 37 | Nc : int 38 | Control horizon. It must be lower or equal to Np. If None, it is set equal to Np. 39 | x0 : 1D array_like. Size: (nx,) 40 | System state at time instant 0. If None, it is set to np.zeros(nx) 41 | xref : 1D array-like. Size: (nx,) 42 | System state reference (aka target, set-point). 43 | uref : 1D array-like. Size: (nu, ). 44 | System input reference. If None, it is set to np.zeros(nx) 45 | uminus1 : 1D array_like 46 | Input value assumed at time instant -1. If None, it is set to uref. 47 | Qx : 2D array_like 48 | State weight matrix. If None, it is set to eye(nx). 49 | Qu : 2D array_like 50 | Input weight matrix. If None, it is set to zeros((nu,nu)). 51 | QDu : 2D array_like 52 | Input delta weight matrix. If None, it is set to zeros((nu,nu)). 53 | xmin : 1D array_like 54 | State minimum value. If None, it is set to -np.inf*ones(nx). 55 | xmax : 1D array_like 56 | State maximum value. If None, it is set to np.inf*ones(nx). 57 | umin : 1D array_like 58 | Input minimum value. If None, it is set to -np.inf*ones(nx). 59 | umax : 1D array_like 60 | Input maximum value. If None, it is set to np.inf*ones(nx). 61 | Dumin : 1D array_like 62 | Input variation minimum value. If None, it is set to np.inf*ones(nx). 63 | Dumax : 1D array_like 64 | Input variation maximum value. If None, it is set to np.inf*ones(nx). 65 | eps_feas : float 66 | Scale factor for the matrix Q_eps. Q_eps = eps_feas*eye(nx). 67 | eps_rel : float 68 | Relative tolerance of the QP solver. Default value: 1e-3. 69 | eps_abs : float 70 | Absolute tolerance of the QP solver. Default value: 1e-3. 71 | """ 72 | 73 | def __init__(self, Ad, Bd, Np=20, Nc=None, 74 | x0=None, xref=None, uref=None, uminus1=None, 75 | Qx=None, QxN=None, Qu=None, QDu=None, 76 | xmin=None, xmax=None, umin=None,umax=None,Dumin=None,Dumax=None, 77 | eps_feas=1e6, eps_rel=1e-3, eps_abs=1e-3): 78 | 79 | if __is_matrix__(Ad) and (Ad.shape[0] == Ad.shape[1]): 80 | self.Ad = Ad 81 | self.nx = Ad.shape[0] # number of states 82 | else: 83 | raise ValueError("Ad should be a square matrix of dimension (nx,nx)!") 84 | 85 | if __is_matrix__(Bd) and Bd.shape[0] == self.nx: 86 | self.Bd = Bd 87 | self.nu = Bd.shape[1] # number of inputs 88 | else: 89 | raise ValueError("Bd should be a matrix of dimension (nx, nu)!") 90 | 91 | if Np > 1: 92 | self.Np = Np # assert 93 | else: 94 | raise ValueError("Np should be > 1!") 95 | 96 | if Nc is not None: 97 | if Nc <= Np: 98 | self.Nc = Nc 99 | else: 100 | raise ValueError("Nc should be <= Np!") 101 | else: 102 | self.Nc = self.Np 103 | 104 | # x0 handling 105 | if x0 is not None: 106 | if __is_vector__(x0) and x0.size == self.nx: 107 | self.x0 = x0.ravel() 108 | else: 109 | raise ValueError("nx should be an array of dimension (nx,)!") 110 | else: 111 | self.x0 = np.zeros(self.nx) 112 | 113 | # reference handing 114 | if xref is not None: 115 | if __is_vector__(xref) and xref.size == self.nx: 116 | self.xref = xref.ravel() 117 | elif __is_matrix__(xref) and xref.shape[1] == self.nx and xref.shape[0] >= self.Np: 118 | self.xref = xref 119 | else: 120 | raise ValueError("xref should be either a vector of shape (nx,) or a matrix of shape (Np+1, nx)!") 121 | else: 122 | self.xref = np.zeros(self.nx) 123 | 124 | if uref is not None: 125 | if __is_vector__(uref) and uref.size == self.nu: 126 | self.uref = uref.ravel() # assert... 127 | else: 128 | raise ValueError("uref should be a vector of shape (nu,)!") 129 | else: 130 | self.uref = np.zeros(self.nu) 131 | 132 | if uminus1 is not None: 133 | if __is_vector__(uminus1) and uminus1.size == self.nu: 134 | self.uminus1 = uminus1 135 | else: 136 | raise ValueError("uminus1 should be a vector of shape (nu,)!") 137 | else: 138 | self.uminus1 = self.uref 139 | 140 | # weights handling 141 | if Qx is not None: 142 | if __is_matrix__(Qx) and Qx.shape[0] == self.nx and Qx.shape[1] == self.nx: 143 | self.Qx = Qx 144 | else: 145 | raise ValueError("Qx should be a matrix of shape (nx,nx)!") 146 | else: 147 | self.Qx = np.zeros((self.nx, self.nx)) # sparse 148 | 149 | if QxN is not None: 150 | if __is_matrix__(QxN) and QxN.shape[0] == self.nx and Qx.shape[1] == self.nx: 151 | self.QxN = QxN 152 | else: 153 | raise ValueError("QxN should be a square matrix of shape (nx,nx)!") 154 | else: 155 | self.QxN = self.Qx # sparse 156 | 157 | if Qu is not None: 158 | if __is_matrix__(Qu) and Qu.shape[0] == self.nu and Qu.shape[1] == self.nu: 159 | self.Qu = Qu 160 | else: 161 | raise ValueError("Qu should be a square matrix of shape (nu,nu)!") 162 | else: 163 | self.Qu = np.zeros((self.nu, self.nu)) 164 | 165 | if QDu is not None: 166 | if __is_matrix__(QDu) and QDu.shape[0] == self.nu and QDu.shape[1] == self.nu: 167 | self.QDu = QDu 168 | else: 169 | raise ValueError("QDu should be a square matrix of shape (nu, nu)!") 170 | else: 171 | self.QDu = np.zeros((self.nu, self.nu)) 172 | 173 | # constraints handling 174 | if xmin is not None: 175 | if __is_vector__(xmin) and xmin.size == self.nx: 176 | self.xmin = xmin.ravel() # assert... 177 | else: 178 | raise ValueError("xmin should be a vector of shape (nx,)!") 179 | else: 180 | self.xmin = -np.ones(self.nx)*np.inf 181 | 182 | if xmax is not None: 183 | if __is_vector__(xmax) and xmax.size == self.nx: 184 | self.xmax = xmax # assert... 185 | else: 186 | raise ValueError("xmax should be a vector of shape (nx,)!") 187 | else: 188 | self.xmax = np.ones(self.nx)*np.inf 189 | 190 | if umin is not None: 191 | if __is_vector__(umin) and umin.size == self.nu: 192 | self.umin = umin # assert... 193 | else: 194 | raise ValueError("umin should be a vector of shape (nu,)!") 195 | else: 196 | self.umin = -np.ones(self.nu)*np.inf 197 | 198 | if umax is not None: 199 | if __is_vector__(umax) and umax.size == self.nu: 200 | self.umax = umax # assert... 201 | else: 202 | raise ValueError("umax should be a vector of shape (nu,)!") 203 | else: 204 | self.umax = np.ones(self.nu)*np.inf 205 | 206 | if Dumin is not None: 207 | if __is_vector__(Dumin) and Dumin.size == self.nu: 208 | self.Dumin = Dumin # assert... 209 | else: 210 | raise ValueError("Dumin should be a vector of shape (nu,)!") 211 | else: 212 | self.Dumin = -np.ones(self.nu)*np.inf 213 | 214 | if Dumax is not None: 215 | if __is_vector__(Dumax) and Dumax.size == self.nu: 216 | self.Dumax = Dumax # assert... 217 | else: 218 | raise ValueError("Dumax should be a vector of shape (nu,)!") 219 | else: 220 | self.Dumax = np.ones(self.nu)*np.inf 221 | 222 | self.eps_feas = eps_feas 223 | self.Qeps = eps_feas * sparse.eye(self.nx) 224 | 225 | self.eps_rel = eps_rel 226 | self.eps_abs = eps_abs 227 | 228 | self.u_failure = self.uref # value provided when the MPC solver fails. 229 | 230 | self.raise_error = False # Raise an error when MPC optimization fails 231 | self.JX_ON = True # Cost function terms in X active 232 | self.JU_ON = True # Cost function terms in U active 233 | self.JDU_ON = True # Cost function terms in Delta U active 234 | self.SOFT_ON = True # Soft constraints active 235 | 236 | self.prob = osqp.OSQP() 237 | 238 | self.res = None 239 | self.P = None 240 | self.q = None 241 | self.A = None 242 | self.l = None 243 | self.u = None 244 | self.x0_rh = None 245 | self.uminus1_rh = None 246 | self.J_CNST = None # Constant term of the cost function 247 | 248 | def setup(self, solve=True): 249 | """ Set-up the QP problem. 250 | 251 | Parameters 252 | ---------- 253 | solve : bool 254 | If True, also solve the QP problem. 255 | 256 | """ 257 | self.x0_rh = np.copy(self.x0) 258 | self.uminus1_rh = np.copy(self.uminus1) 259 | self._compute_QP_matrices_() 260 | self.prob.setup(self.P, self.q, self.A, self.l, self.u, warm_start=True, verbose=False, eps_abs=self.eps_rel, eps_rel=self.eps_abs) 261 | 262 | if solve: 263 | self.solve() 264 | 265 | def output(self, return_x_seq=False, return_u_seq=False, return_eps_seq=False, return_status=False, return_obj_val=False): 266 | """ Return the MPC controller output uMPC, i.e., the first element of the optimal input sequence and assign is to self.uminus1_rh. 267 | 268 | 269 | Parameters 270 | ---------- 271 | return_x_seq : bool 272 | If true, the method also returns the optimal sequence of states 273 | 274 | Returns 275 | ------- 276 | array_like (nu,) 277 | The first element of the optimal input sequence uMPC to be applied to the system. 278 | """ 279 | Nc = self.Nc 280 | Np = self.Np 281 | nx = self.nx 282 | nu = self.nu 283 | 284 | # Extract first control input to the plant 285 | if self.res.info.status == 'solved': 286 | uMPC = self.res.x[(Np+1)*nx:(Np+1)*nx + nu] 287 | else: 288 | uMPC = self.u_failure 289 | 290 | # Return additional info? 291 | info = {} 292 | if return_x_seq: 293 | seq_X = self.res.x[0:(Np+1)*nx] 294 | seq_X = seq_X.reshape(-1,nx) 295 | info['x_seq'] = seq_X 296 | 297 | if return_u_seq: 298 | seq_U = self.res.x[(Np+1)*nx:(Np+1)*nx + Nc*nu] 299 | seq_U = seq_U.reshape(-1,nu) 300 | info['u_seq'] = seq_U 301 | 302 | if return_eps_seq: 303 | seq_eps = self.res.x[(Np+1)*nx + Nc*nu : (Np+1)*nx + Nc*nu + (Np+1)*nx ] 304 | seq_eps = seq_eps.reshape(-1,nx) 305 | info['eps_seq'] = seq_eps 306 | 307 | if return_status: 308 | info['status'] = self.res.info.status 309 | 310 | if return_obj_val: 311 | obj_val = self.res.info.obj_val + self.J_CNST # constant of the objective value 312 | info['obj_val'] = obj_val 313 | 314 | self.uminus1_rh = uMPC 315 | 316 | if len(info) == 0: 317 | return uMPC 318 | 319 | else: 320 | return uMPC, info 321 | 322 | def update(self,x,u=None,xref=None,solve=True): 323 | """ Update the QP problem. 324 | 325 | Parameters 326 | ---------- 327 | x : array_like. Size: (nx,) 328 | The new value of x0. 329 | 330 | u : array_like. Size: (nu,) 331 | The new value of uminus1. If none, it is set to the previously computed u. 332 | 333 | xref : array_like. Size: (nx,) 334 | The new value of xref. If none, it is not changed 335 | 336 | solve : bool 337 | If True, also solve the QP problem. 338 | 339 | """ 340 | 341 | self.x0_rh = x # previous x0 342 | if u is not None: 343 | self.uminus1_rh = u # otherwise it is just the uMPC updated from the previous step() call 344 | if xref is not None: 345 | self.xref = xref # TODO: check that new reference is != old reference, do a minimal update of the QP matrices to improve speed 346 | self._update_QP_matrices_() 347 | if solve: 348 | self.solve() 349 | 350 | def solve(self): 351 | """ Solve the QP problem. """ 352 | 353 | self.res = self.prob.solve() 354 | 355 | # Check solver status 356 | if self.res.info.status != 'solved': 357 | warnings.warn('OSQP did not solve the problem!') 358 | if self.raise_error: 359 | raise ValueError('OSQP did not solve the problem!') 360 | 361 | def __controller_function__(self, x, u, xref=None): 362 | """ This function is meant to be used for debug only. 363 | """ 364 | 365 | self.update(x,u,xref=xref,solve=True) 366 | uMPC = self.output() 367 | 368 | return uMPC 369 | 370 | 371 | def _update_QP_matrices_(self): 372 | x0_rh = self.x0_rh 373 | uminus1_rh = self.uminus1_rh 374 | Np = self.Np 375 | Nc = self.Nc 376 | nx = self.nx 377 | nu = self.nu 378 | Dumin = self.Dumin 379 | Dumax = self.Dumax 380 | QDu = self.QDu 381 | uref = self.uref 382 | Qeps = self.Qeps 383 | Qx = self.Qx 384 | QxN = self.QxN 385 | Qu = self.Qu 386 | xref = self.xref 387 | P_X = self.P_X 388 | 389 | self.l[:nx] = -x0_rh 390 | self.u[:nx] = -x0_rh 391 | 392 | self.l[(Np+1)*nx + (Np+1)*nx + (Nc)*nu:(Np+1)*nx + (Np+1)*nx + (Nc)*nu + nu] = Dumin + uminus1_rh[0:nu] # update constraint on \Delta u0: Dumin <= u0 - u_{-1} 393 | self.u[(Np+1)*nx + (Np+1)*nx + (Nc)*nu:(Np+1)*nx + (Np+1)*nx + (Nc)*nu + nu] = Dumax + uminus1_rh[0:nu] # update constraint on \Delta u0: u0 - u_{-1} <= Dumax 394 | 395 | # Update the linear term q. This part could be further optimized in case of constant xref... 396 | q_X = np.zeros((Np + 1) * nx) # x_N 397 | self.J_CNST = 0.0 398 | if self.JX_ON: 399 | # self.J_CNST += 1/2*Np*(xref.dot(QxN.dot(xref))) + 1/2*xref.dot(QxN.dot(xref)) # TODO adjust for non-constant xref 400 | 401 | if xref.ndim == 2 and xref.shape[0] >= Np + 1: # xref is a vector per time-instant! experimental feature 402 | #for idx_ref in range(Np): 403 | # q_X[idx_ref * nx:(idx_ref + 1) * nx] += -Qx.dot(xref[idx_ref, :]) 404 | #q_X[Np * nx:(Np + 1) * nx] += -QxN.dot(xref[Np, :]) 405 | q_X += (-xref.reshape(1, -1) @ (P_X)).ravel() # way faster implementation of the same formula commented above 406 | else: 407 | q_X += np.hstack([np.kron(np.ones(Np), -Qx.dot(xref)), # x0... x_N-1 408 | -QxN.dot(xref)]) # x_N 409 | else: 410 | pass 411 | 412 | q_U = np.zeros(Nc*nu) 413 | if self.JU_ON: 414 | self.J_CNST += 1/2* Np * (uref.dot(Qu.dot(uref))) 415 | if self.Nc == self.Np: 416 | q_U += np.kron(np.ones(Nc), -Qu.dot(uref)) 417 | else: # Nc < Np. This formula is more general and could handle the case Nc = Np either. TODO: test 418 | iU = np.ones(Nc) 419 | iU[Nc-1] = (Np - Nc + 1) 420 | q_U += np.kron(iU, -Qu.dot(uref)) 421 | 422 | # Filling P and q for J_DU 423 | if self.JDU_ON: 424 | self.J_CNST += 1/2*uminus1_rh.dot((QDu).dot(uminus1_rh)) 425 | q_U += np.hstack([-QDu.dot(uminus1_rh), # u0 426 | np.zeros((Nc - 1) * nu)]) # u1..uN-1 427 | else: 428 | pass 429 | 430 | if self.SOFT_ON: 431 | q_eps = np.zeros((Np+1)*nx) 432 | self.q = np.hstack([q_X, q_U, q_eps]) 433 | else: 434 | self.q = np.hstack([q_X, q_U]) 435 | 436 | self.prob.update(l=self.l, u=self.u, q=self.q) 437 | 438 | def _compute_QP_matrices_(self): 439 | Np = self.Np 440 | Nc = self.Nc 441 | nx = self.nx 442 | nu = self.nu 443 | Qx = self.Qx 444 | QxN = self.QxN 445 | Qu = self.Qu 446 | QDu = self.QDu 447 | xref = self.xref 448 | uref = self.uref 449 | uminus1 = self.uminus1 450 | Ad = self.Ad 451 | Bd = self.Bd 452 | x0 = self.x0 453 | xmin = self.xmin 454 | xmax = self.xmax 455 | umin = self.umin 456 | umax = self.umax 457 | Dumin = self.Dumin 458 | Dumax = self.Dumax 459 | Qeps = self.Qeps 460 | 461 | # Cast MPC problem to a QP: x = (x(0),x(1),...,x(N),u(0),...,u(N-1)) 462 | # - quadratic objective 463 | 464 | P_X = sparse.csc_matrix(((Np+1)*nx, (Np+1)*nx)) 465 | q_X = np.zeros((Np+1)*nx) # x_N 466 | self.J_CNST = 0.0 467 | if self.JX_ON: 468 | P_X += sparse.block_diag([sparse.kron(sparse.eye(Np), Qx), # x0...x_N-1 469 | QxN]) # xN 470 | 471 | if xref.ndim == 2 and xref.shape[0] >= Np + 1: # xref is a vector per time-instant! experimental feature 472 | #for idx_ref in range(Np): 473 | # q_X[idx_ref * nx:(idx_ref + 1) * nx] += -Qx.dot(xref[idx_ref, :]) 474 | #q_X[Np * nx:(Np + 1) * nx] += -QxN.dot(xref[Np, :]) 475 | q_X += (-xref.reshape(1, -1) @ (P_X)).ravel() 476 | else: 477 | q_X += np.hstack([np.kron(np.ones(Np), -Qx.dot(xref)), # x0... x_N-1 478 | -QxN.dot(xref)]) # x_N 479 | 480 | # self.J_CNST += 1/2*Np*(xref.dot(QxN.dot(xref))) + 1/2*xref.dot(QxN.dot(xref)) # TODO adapt for non-constant xref 481 | else: 482 | pass 483 | 484 | # Filling P and q for J_U 485 | P_U = sparse.csc_matrix((Nc*nu, Nc*nu)) 486 | q_U = np.zeros(Nc*nu) 487 | if self.JU_ON: 488 | self.J_CNST += 1/2*Np*(uref.dot(Qu.dot(uref))) 489 | 490 | if self.Nc == self.Np: 491 | P_U += sparse.kron(sparse.eye(Nc), Qu) 492 | q_U += np.kron(np.ones(Nc), -Qu.dot(uref)) 493 | else: # Nc < Np. This formula is more general and could handle the case Nc = Np either. TODO: test 494 | iU = np.ones(Nc) 495 | iU[Nc-1] = (Np - Nc + 1) 496 | P_U += sparse.kron(sparse.diags(iU), Qu) 497 | q_U += np.kron(iU, -Qu.dot(uref)) 498 | 499 | # Filling P and q for J_DU 500 | if self.JDU_ON: 501 | self.J_CNST += 1/2*uminus1.dot((QDu).dot(uminus1)) 502 | iDu = 2 * np.eye(Nc) - np.eye(Nc, k=1) - np.eye(Nc, k=-1) 503 | iDu[Nc - 1, Nc - 1] = 1 504 | P_U += sparse.kron(iDu, QDu) 505 | q_U += np.hstack([-QDu.dot(uminus1), # u0 506 | np.zeros((Nc - 1) * nu)]) # u1..uN-1 507 | else: 508 | pass 509 | 510 | if self.SOFT_ON: 511 | P_eps = sparse.kron(np.eye((Np+1)), Qeps) 512 | q_eps = np.zeros((Np+1)*nx) 513 | 514 | # Linear constraints 515 | 516 | # - linear dynamics x_k+1 = Ax_k + Bu_k 517 | Ax = sparse.kron(sparse.eye(Np + 1), -sparse.eye(nx)) + sparse.kron(sparse.eye(Np + 1, k=-1), Ad) 518 | iBu = sparse.vstack([sparse.csc_matrix((1, Nc)), 519 | sparse.eye(Nc)]) 520 | if self.Nc < self.Np: # expand A matrix if Nc < Nu (see notes) 521 | iBu = sparse.vstack([iBu, 522 | sparse.hstack([sparse.csc_matrix((Np - Nc, Nc - 1)), np.ones((Np - Nc, 1))]) 523 | ]) 524 | Bu = sparse.kron(iBu, Bd) 525 | 526 | n_eps = (Np + 1) * nx 527 | Aeq_dyn = sparse.hstack([Ax, Bu]) 528 | if self.SOFT_ON: 529 | Aeq_dyn = sparse.hstack([Aeq_dyn, sparse.csc_matrix((Aeq_dyn.shape[0], n_eps))]) # For soft constraints slack variables 530 | 531 | leq_dyn = np.hstack([-x0, np.zeros(Np * nx)]) 532 | ueq_dyn = leq_dyn # for equality constraints -> upper bound = lower bound! 533 | 534 | # - bounds on x 535 | Aineq_x = sparse.hstack([sparse.eye((Np + 1) * nx), sparse.csc_matrix(((Np+1)*nx, Nc*nu))]) 536 | if self.SOFT_ON: 537 | Aineq_x = sparse.hstack([Aineq_x, sparse.eye(n_eps)]) # For soft constraints slack variables 538 | lineq_x = np.kron(np.ones(Np + 1), xmin) # lower bound of inequalities 539 | uineq_x = np.kron(np.ones(Np + 1), xmax) # upper bound of inequalities 540 | 541 | Aineq_u = sparse.hstack([sparse.csc_matrix((Nc*nu, (Np+1)*nx)), sparse.eye(Nc * nu)]) 542 | if self.SOFT_ON: 543 | Aineq_u = sparse.hstack([Aineq_u, sparse.csc_matrix((Aineq_u.shape[0], n_eps))]) # For soft constraints slack variables 544 | lineq_u = np.kron(np.ones(Nc), umin) # lower bound of inequalities 545 | uineq_u = np.kron(np.ones(Nc), umax) # upper bound of inequalities 546 | 547 | 548 | # - bounds on \Delta u 549 | Aineq_du = sparse.vstack([sparse.hstack([np.zeros((Np + 1) * nx), np.ones(nu), np.zeros((Nc - 1) * nu)]), # for u0 - u-1 550 | sparse.hstack([np.zeros((Nc * nu, (Np+1) * nx)), -sparse.eye(Nc * nu) + sparse.eye(Nc * nu, k=1)]) # for uk - uk-1, k=1...Np 551 | ] 552 | ) 553 | if self.SOFT_ON: 554 | Aineq_du = sparse.hstack([Aineq_du, sparse.csc_matrix((Aineq_du.shape[0], n_eps))]) 555 | 556 | uineq_du = np.ones((Nc+1) * nu)*Dumax 557 | uineq_du[0:nu] += self.uminus1[0:nu] 558 | 559 | lineq_du = np.ones((Nc+1) * nu)*Dumin 560 | lineq_du[0:nu] += self.uminus1[0:nu] # works for nonscalar u? 561 | 562 | # Positivity of slack variables (not necessary!) 563 | #Aineq_eps_pos = sparse.hstack([sparse.coo_matrix((n_eps,(Np+1)*nx)), sparse.coo_matrix((n_eps, Np*nu)), sparse.eye(n_eps)]) 564 | #lineq_eps_pos = np.zeros(n_eps) 565 | #uineq_eps_pos = np.ones(n_eps)*np.inf 566 | 567 | # - OSQP constraints 568 | #A = sparse.vstack([Aeq_dyn, Aineq_x, Aineq_u, Aineq_du, Aineq_eps_pos]).tocsc() 569 | #l = np.hstack([leq_dyn, lineq_x, lineq_u, lineq_du, lineq_eps_pos]) 570 | #u = np.hstack([ueq_dyn, uineq_x, uineq_u, uineq_du, uineq_eps_pos]) 571 | 572 | A = sparse.vstack([Aeq_dyn, Aineq_x, Aineq_u, Aineq_du]).tocsc() 573 | l = np.hstack([leq_dyn, lineq_x, lineq_u, lineq_du]) 574 | u = np.hstack([ueq_dyn, uineq_x, uineq_u, uineq_du]) 575 | 576 | # assign all 577 | if self.SOFT_ON: 578 | self.P = sparse.block_diag([P_X, P_U, P_eps], format='csc') 579 | self.q = np.hstack([q_X, q_U, q_eps]) 580 | else: 581 | self.P = sparse.block_diag([P_X, P_U],format='csc') 582 | self.q = np.hstack([q_X, q_U]) 583 | 584 | self.A = A 585 | self.l = l 586 | self.u = u 587 | 588 | self.P_X = P_X 589 | # Debug assignments 590 | 591 | # self.P_x = P_X 592 | # self.P_U = P_U 593 | # self.P_eps = P_eps 594 | #self.Aineq_du = Aineq_du 595 | #self.leq_dyn = leq_dyn 596 | #self.lineq_du = lineq_du 597 | 598 | if __name__ == '__main__': 599 | import time 600 | import matplotlib.pyplot as plt 601 | # Constants # 602 | Ts = 0.2 # sampling time (s) 603 | M = 2 # mass (Kg) 604 | b = 0.3 # friction coefficient (N*s/m) 605 | 606 | Ad = sparse.csc_matrix([ 607 | [1.0, Ts], 608 | [0, 1.0 -b/M*Ts] 609 | ]) 610 | Bd = sparse.csc_matrix([ 611 | [0.0], 612 | [Ts/M]]) 613 | 614 | # Continous-time matrices (just for reference) 615 | Ac = np.array([ 616 | [0.0, 1.0], 617 | [0, -b/M]] 618 | ) 619 | Bc = np.array([ 620 | [0.0], 621 | [1/M] 622 | ]) 623 | 624 | # Reference input and states 625 | pref = 7.0 626 | vref = 0.0 627 | xref = np.array([pref, vref]) # reference state 628 | uref = np.array([0.0]) # reference input 629 | uminus1 = np.array([0.0]) # input at time step negative one - used to penalize the first delta u at time instant 0. Could be the same as uref. 630 | 631 | # Constraints 632 | xmin = np.array([-10, -10.0]) 633 | xmax = np.array([7.0, 10.0]) 634 | 635 | umin = np.array([-1.2]) 636 | umax = np.array([1.2]) 637 | 638 | Dumin = np.array([-2e-1]) 639 | Dumax = np.array([2e-1]) 640 | 641 | # Objective function 642 | Qx = sparse.diags([0.5, 0.1]) # Quadratic cost for states x0, x1, ..., x_N-1 643 | QxN = sparse.diags([0.5, 0.1]) # Quadratic cost for xN 644 | Qu = 2.0 * sparse.eye(1) # Quadratic cost for u0, u1, ...., u_N-1 645 | QDu = 10.0 * sparse.eye(1) # Quadratic cost for Du0, Du1, ...., Du_N-1 646 | 647 | # Initial state 648 | x0 = np.array([0.1, 0.2]) # initial state 649 | 650 | # Prediction horizon 651 | Np = 25 652 | Nc = 10 653 | 654 | Xref = np.kron(np.ones((Np + 1,1)), xref) 655 | K = MPCController(Ad,Bd,Np=Np,Nc=Nc,x0=x0,xref=Xref,uminus1=uminus1, 656 | Qx=Qx, QxN=QxN, Qu=Qu,QDu=QDu, 657 | xmin=xmin,xmax=xmax,umin=umin,umax=umax,Dumin=Dumin,Dumax=Dumax) 658 | K.setup() 659 | 660 | # Simulate in closed loop 661 | [nx, nu] = Bd.shape # number of states and number or inputs 662 | len_sim = 40 # simulation length (s) 663 | nsim = int(len_sim/Ts) # simulation length(timesteps) 664 | xsim = np.zeros((nsim,nx)) 665 | usim = np.zeros((nsim,nu)) 666 | tsim = np.arange(0,nsim)*Ts 667 | 668 | time_start = time.time() 669 | xstep = x0 670 | for i in range(nsim): 671 | uMPC, info = K.output(return_u_seq=True, return_x_seq=True, return_eps_seq=True, return_status=True) 672 | xstep = Ad.dot(xstep) + Bd.dot(uMPC) # system step 673 | K.update(xstep, xref=Xref) # update with measurement 674 | K.solve() 675 | xsim[i,:] = xstep 676 | usim[i,:] = uMPC 677 | 678 | time_sim = time.time() - time_start 679 | 680 | #K.__controller_function__(np.array([0,0]), np.array([0])) 681 | 682 | fig,axes = plt.subplots(3,1, figsize=(10,10)) 683 | axes[0].plot(tsim, xsim[:,0], "k", label='p') 684 | axes[0].plot(tsim, xref[0]*np.ones(np.shape(tsim)), "r--", label="pref") 685 | axes[0].set_title("Position (m)") 686 | 687 | axes[1].plot(tsim, xsim[:,1], label="v") 688 | axes[1].plot(tsim, xref[1]*np.ones(np.shape(tsim)), "r--", label="vref") 689 | axes[1].set_title("Velocity (m/s)") 690 | 691 | axes[2].plot(tsim, usim[:,0], label="u") 692 | axes[2].plot(tsim, uref*np.ones(np.shape(tsim)), "r--", label="uref") 693 | axes[2].set_title("Force (N)") 694 | 695 | for ax in axes: 696 | ax.grid(True) 697 | ax.legend() 698 | -------------------------------------------------------------------------------- /slides/biblio.bib: -------------------------------------------------------------------------------- 1 | @article{bombois2006least, 2 | title={Least costly identification experiment for control}, 3 | author={Bombois, X. and Scorletti, G. and Gevers, M. and Van den Hof, PMJ and Hildebrand, R.}, 4 | journal={Automatica}, 5 | volume={42}, 6 | number={10}, 7 | pages={1651--1662}, 8 | year={2006}, 9 | publisher={Elsevier} 10 | } -------------------------------------------------------------------------------- /slides/img/GLIS/GLIS_06.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/GLIS/GLIS_06.pdf -------------------------------------------------------------------------------- /slides/img/GLIS/GLIS_07.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/GLIS/GLIS_07.pdf -------------------------------------------------------------------------------- /slides/img/GLIS/GLIS_08.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/GLIS/GLIS_08.pdf -------------------------------------------------------------------------------- /slides/img/GLIS/acquisition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/GLIS/acquisition.png -------------------------------------------------------------------------------- /slides/img/GLIS/surrogate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/GLIS/surrogate.png -------------------------------------------------------------------------------- /slides/img/MPCloop.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/MPCloop.pdf -------------------------------------------------------------------------------- /slides/img/MPCloop.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 28 | 33 | 34 | 42 | 47 | 48 | 56 | 61 | 62 | 70 | 75 | 76 | 84 | 89 | 90 | 93 | 96 | 99 | 107 | 112 | 113 | 121 | 126 | 127 | 130 | 133 | 134 | 153 | 156 | 157 | 159 | 160 | 162 | image/svg+xml 163 | 165 | 166 | 167 | 168 | 169 | 173 | 180 | 187 | 192 | 197 | 202 | MPC 213 | 221 | 229 | 237 | 245 | 249 | 256 | 261 | 266 | EST 277 | 280 | 287 | 294 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /slides/img/example/BEST_GLIS_PC.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/example/BEST_GLIS_PC.pdf -------------------------------------------------------------------------------- /slides/img/example/BEST_GLIS_PI.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/example/BEST_GLIS_PI.pdf -------------------------------------------------------------------------------- /slides/img/example/COMPUTATION_BO_PI.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/example/COMPUTATION_BO_PI.pdf -------------------------------------------------------------------------------- /slides/img/example/COMPUTATION_GLIS_PI.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/example/COMPUTATION_GLIS_PI.pdf -------------------------------------------------------------------------------- /slides/img/example/HIL_PC.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/example/HIL_PC.pdf -------------------------------------------------------------------------------- /slides/img/example/HIL_PI.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/example/HIL_PI.pdf -------------------------------------------------------------------------------- /slides/img/example/ITER_GLIS_PC_500.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/example/ITER_GLIS_PC_500.pdf -------------------------------------------------------------------------------- /slides/img/example/ITER_GLIS_PI_500.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/example/ITER_GLIS_PI_500.pdf -------------------------------------------------------------------------------- /slides/img/example/cart_pole.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/example/cart_pole.pdf -------------------------------------------------------------------------------- /slides/img/teaser.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/img/teaser.pdf -------------------------------------------------------------------------------- /slides/preamble.tex: -------------------------------------------------------------------------------- 1 | % does not look nice, try deleting the line with the fontenc. 2 | \usepackage[english]{babel} 3 | \usepackage{amsmath} 4 | \usepackage[latin1]{inputenc} 5 | \usepackage{units} 6 | \usepackage{colortbl} 7 | \usepackage{multimedia} 8 | \usepackage{bm} 9 | 10 | \mode 11 | { 12 | \usetheme{Boadilla} 13 | \useoutertheme{infolines} 14 | \setbeamercovered{transparent} 15 | } 16 | 17 | 18 | \title[Efficient Calibration of Embedded MPC]{\textsc{Efficient Calibration of Embedded MPC}} 19 | 20 | %\subtitle{Industrial-scale experimental results\\} % (optional) 21 | 22 | \author[]{Marco Forgione\inst{1}, Dario Piga\inst{1}, 23 | and Alberto Bemporad \inst{2}} 24 | 25 | \institute[IDSIA]{ 26 | \inst{1}IDSIA Dalle Molle Institute for Artificial Intelligence SUPSI-USI, Lugano, Switzerland \and 27 | \inst{2}IMT School for Advanced Studies Lucca, Lucca, Italy 28 | } 29 | 30 | 31 | \date[IFAC 2020]{IFAC 2020 World Congress\\ Berlin, Germany} 32 | 33 | 34 | \subject{Performance-oriented MPC tuning} 35 | 36 | 37 | %% MATH DEFINITIONS %% 38 | %\newcommand{\R}{\mathcal{R}} 39 | %\newcommand{\du}{\delta u} 40 | %\newcommand{\dy}{\delta y} 41 | %\newcommand{\DU}{\Delta U} 42 | %\newcommand{\DY}{\Delta Y} 43 | %\newcommand{\abs}[1]{\left | #1 \right |} 44 | \newcommand{\norm}[1]{\left \lVert #1 \right \rVert} 45 | %\newcommand{\relphantom}[1]{\mathrel{\phantom{#1}}} 46 | %\newenvironment{matrixc}{\begin{array}{c}\left[}{\end{array}\right]} 47 | \DeclareMathOperator*\argmin{arg \, min} 48 | %\DeclareMathOperator*\argmax{arg \, max} 49 | %\DeclareMathOperator*\fit{fit} 50 | %\DeclareMathOperator*\RMSE{RMSE} 51 | %\DeclareMathOperator*\diag{diag} 52 | %\DeclareMathOperator*\diet{diet} 53 | %\DeclareMathOperator*\Risk{Risk} 54 | %\DeclareMathOperator*\Num{Num} 55 | %\DeclareMathOperator*\Den{Den} 56 | %\DeclareMathOperator*\Rat{Rat} 57 | \DeclareMathOperator*\cov{cov} 58 | %\DeclareMathOperator*\Var{Var} 59 | %\DeclareMathOperator*\SSR{SSR} 60 | %\setcounter{MaxMatrixCols}{20} 61 | %\newcommand{\pdiff}[2]{\frac{\partial #1}{\partial #2}} 62 | %\definecolor{mypink1}{rgb}{0.858, 0.188, 0.478} 63 | %\definecolor{olive}{RGB}{85, 107, 47} 64 | \definecolor{orange}{RGB}{204, 85, 0} 65 | 66 | %\definecolor{mypink3}{cmyk}{0, 0.7808, 0.4429, 0.1412} 67 | %\definecolor{mygray}{gray}{0.6} 68 | %\definecolor{olivegreen}[RGB]{85, 107, 47} 69 | 70 | \newcommand{\K}{K} 71 | \newcommand{\M}{M} 72 | \newcommand{\Mo}{M_o} 73 | \newcommand{\So}{S_o} 74 | \newcommand{\Smod}{S} 75 | \newcommand{\parcolor}[1]{{\color{orange}#1}} 76 | \newcommand{\Ts}{T_s^{\rm MPC}} 77 | \newcommand{\cites}[1]{\begin{small}(#1)\end{small}} 78 | %\newcommand{\Ts}{T_s} 79 | -------------------------------------------------------------------------------- /slides/presentation_main.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forgi86/efficient-calibration-embedded-MPC/ac42a7ebe9e394e35deda7e72d856c2033dff434/slides/presentation_main.pdf -------------------------------------------------------------------------------- /slides/presentation_main.tex: -------------------------------------------------------------------------------- 1 | \documentclass{beamer} 2 | \usefonttheme[onlymath]{serif} 3 | \input{preamble} 4 | \definecolor{darkgreen}{RGB}{20,150,50} 5 | \definecolor{acq}{RGB}{119, 172, 48} 6 | \definecolor{true}{RGB}{162, 20, 47} 7 | \definecolor{surr}{RGB}{0, 114, 189} 8 | 9 | \begin{document} 10 | 11 | \begin{frame} 12 | \titlepage 13 | \end{frame} 14 | 15 | \begin{frame}{Motivations} 16 | Calibration of \structure{model predictive controllers} is \alert{costly} and \alert{time-consuming}.\\ 17 | One has to choose model, cost function weights, prediction/control horizon, design a state estimator, etc. 18 | %Typically, models are obtained through Physical modeling or Identification 19 | \vskip 1em 20 | The design is even more involved for \structure{embedded applications}, where the hardware is also a limit. Need to choose carefully: 21 | \begin{itemize} 22 | \item Solution strategy (QP solver?) 23 | \item Low-level solver settings (e.g., tolerances) 24 | \item A \structure{feasible} MPC sampling time 25 | \end{itemize} 26 | %\vskip 1em 27 | %Requires \structure{knowledge} in control systems, numerical math, computer engineering + \structure{hands-on experience}. 28 | \vskip 1em 29 | In this paper, we introduce an approach to ease embedded MPC calibration based on \structure{global optimization} and repeated experiements. 30 | \end{frame} 31 | 32 | 33 | \begin{frame}{Control architecture}{Model Predictive Controller} 34 | \begin{figure} 35 | \centering 36 | \includegraphics[width=.3\textwidth]{img/MPCloop.pdf} 37 | \end{figure} 38 | 39 | Linear constrained MPC for a (non-linear) plant with model 40 | \begin{columns} 41 | \column{.3\textwidth} 42 | \begin{small} 43 | \begin{equation*} 44 | \begin{split} 45 | \dot x(t) &= f(x(t), u(t);\; \parcolor{\beta})\\ 46 | y(t) &=g(x(t); \; \parcolor{\beta}) %+\parcolor{D}u_t, 47 | \end{split} 48 | \end{equation*} 49 | \end{small} 50 | \column{.2\textwidth} 51 | $$\Rightarrow$$ 52 | \column{.5\textwidth} 53 | \begin{small} 54 | \begin{equation*} 55 | \begin{split} 56 | x_{t+1}&={A}(\parcolor{\Ts}, \parcolor{\beta})x_t+{B}(\parcolor{\Ts}, \parcolor{\beta})u_t\\ 57 | y_t &=C(\parcolor{\beta})x_t %+\parcolor{D}u_t, 58 | \end{split} 59 | \end{equation*} 60 | \end{small} 61 | \end{columns} 62 | input $u$ chosen on-line as the solution (applied in 63 | {receding horizon}) of: 64 | \vskip -1.5em 65 | \begin{small} 66 | \begin{align*} 67 | &\min_{\left\{u_{t+k|t}\right\}_{k=1}^{\parcolor{N_c}}} \;\;\sum_{k=1}^{\parcolor{N_p}} \norm{\hat y_{t+k|t}-\overline{y}_{t+k}}^2_{\parcolor{Q_y}} 68 | + \norm{u_{t+k|k}\!-\!u_{t+k-1|t}}^2_{\parcolor{Q_{\Delta u}}} \\ 69 | & \text{s.t. model equations + constraints on $y$, $u$, $\Delta u$} 70 | \end{align*} 71 | \end{small} 72 | %\vskip -1.5em 73 | %MPC control law computed on-line using a QP solver.% (OSQP, ADMM-based). 74 | \end{frame} 75 | 76 | \begin{frame}{Control architecture}{Design variables} 77 | The MPC design variables to be tuned are 78 | \begin{columns}[t] 79 | \column{.6\textwidth} 80 | \begin{itemize} 81 | \item Model parameters: $\parcolor{\beta}$ 82 | \item MPC sampling time: $\parcolor{\Ts}$ 83 | \item MPC cost function: $\parcolor{Q_y}, \parcolor{Q_{\Delta u}}, \parcolor{N_c}, \parcolor{N_p}$ 84 | \end{itemize} 85 | \column{.4\textwidth} 86 | \vskip -1em 87 | \begin{figure} 88 | \centering 89 | \includegraphics[width=.75\textwidth]{img/MPCloop.pdf} 90 | \end{figure} 91 | \end{columns} 92 | \begin{itemize} 93 | \item MPC implementation: QP solver tolerances $\parcolor{\epsilon_{\rm abs}}, \parcolor{\epsilon_{\rm rel}}$ 94 | \item State estimator: Kalman covariances $\parcolor{W_v}, \parcolor{W_w}$ 95 | \end{itemize} 96 | %\vskip 1em 97 | %in order to: 98 | %\begin{enumerate} 99 | %\item maximize closed-loop performance %objective $J$ 100 | %\item ensure that the implemented MPC runs in real-time 101 | %\end{enumerate} 102 | %\vskip 1em 103 | %Note: closed-loop performance is not the MPC cost function! 104 | \end{frame} 105 | 106 | 107 | 108 | \begin{frame}{MPC calibration procedure}{Overview} 109 | In our optimization-based MPC calibration, we define 110 | \begin{itemize} 111 | \item Tunable \structure{design parameters} collected in a \parcolor{design vector $\theta$} $\in \Theta$. 112 | \item A closed-loop \structure{performance index $J$} defined in terms of measured input/outputs during the {calibration experiment}: $J = J(y_{1:T},u_{1:T};\;\parcolor{\theta})$ 113 | \item An \structure{procedure} to perform \structure{calibration experiments} (or SIL simulations) representative of the intended closed-loop operation 114 | \end{itemize} 115 | \pause 116 | MPC calibration is seen as a \structure{global optimization} problem: 117 | \begin{align*} 118 | &\theta^{\rm opt} = \argmin_{\theta \in \Theta} J(y_{1:T},u_{1:T};\;{\theta})\\ 119 | & \text{s.t. } T_{\rm calc}^{\rm MPC}({\theta}) \leq {\Ts} 120 | \end{align*} 121 | each (noisy) \structure{function evaluation} correspond to a \structure{calibration experiment}.\\ 122 | Both $J$ and $T_{\rm calc}^{\rm MPC}$ (worst-case MPC computation time) are observed! 123 | %\vskip 1em 124 | %\pause 125 | %Problem is tackled using \structure{efficient} global optimization algorithms. 126 | \end{frame} 127 | 128 | \begin{frame}{Global Optimization Algorithm}{Overview} 129 | Several \structure{derivative-free} global optimization algorithms exist: 130 | \begin{itemize} 131 | \item Response surface methods 132 | \item Bayesian Optimization% (BO) 133 | \item Genetic algorithms 134 | \item Particle Swarm Optimization 135 | \item \dots%Lipshitzian-based partitioning techniques 136 | \end{itemize} 137 | % \end{itemize} 138 | \vskip .5em 139 | \pause 140 | Here, we adopt \structure{GLIS}: a method recently introduced in \cites{Bemporad, 2019}. 141 | \begin{itemize} 142 | \item Radial basis function \structure{surrogate} + inverse distance weighting (IDW) for \structure{exploration} 143 | \item Performs better than BO on standard benchmarks, at a lower computational cost 144 | %\item Matlab- and Python-based implementation of GLIS available 145 | \end{itemize} 146 | \end{frame} 147 | 148 | \begin{frame}{GLIS Algorithm}{Surrogate function} 149 | 150 | \begin{columns}[t] 151 | \column{.55\textwidth} 152 | Goal: solve \structure{global optimization} problem: 153 | % \begin{align*} 154 | $$ \min_{\theta \in \Theta} J(\theta)$$ 155 | %\\ 156 | %& \text{s.t. } \theta \in \Theta 157 | %\end{align*} 158 | %\vskip -.5em 159 | \begin{enumerate} 160 | \item Collect ${n_{\rm in}}$ initial observations $\{(\theta_1, J_1), (\theta_2, J_2), \dots, (\theta_{n_{\rm in}}, J_{n_{\rm in}})\}$ 161 | \item Build a \structure{surrogate function} 162 | \vskip -0.5em 163 | $$\hat J(\theta) = \sum_{i=1}^{n} \alpha_i \phi(\norm{\theta - \theta_i}_2 )$$ 164 | \end{enumerate} 165 | \column{.45\textwidth} 166 | %\vskip -1.5em 167 | \centering 168 | True {\color{true} $J(\theta)$}, surrogate {\color{surr} $\hat{J}(\theta)$} 169 | \begin{figure} 170 | \includegraphics[width=.7\textwidth]{img/GLIS/surrogate.png} 171 | \end{figure} 172 | $ \phi = $ \text{radial basis function}\\ 173 | \vskip .5em 174 | Example: $\phi(d) = \frac{1}{1 + (\epsilon d)^2}$ 175 | \end{columns} 176 | \vskip 1em 177 | %Vector $\alpha$ solves $\hat J(\theta_i) = J_i,\; i=1,\dots,n\;\;$ (= linear system) 178 | \pause 179 | \vskip 1em 180 | Minimizing $\hat J(\theta)$ to find $\theta_{n+1}$ may easily miss the global optimum! 181 | \end{frame} 182 | 183 | \begin{frame}{GLIS Algorithm}{Exploration vs. Exploitation} 184 | \begin{columns}[t] 185 | \column{.57\textwidth} 186 | \begin{enumerate} 187 | \setcounter{enumi}{2} 188 | \item Construct the IDW \structure{exploration function} 189 | \begin{equation*} 190 | z(\theta) = \frac{2}{\pi}\Delta F \tan^{-1}\left(\frac{1}{\sum_{i=1}^n w_i(\theta)} \right) 191 | \end{equation*} 192 | where $w_i(\theta) = \frac{e^{-\norm{\theta - \theta_i}^2}}{\norm{\theta - \theta_i}^2}$%{-\norm{x - x_i}}^2%}{1}$ 193 | \item Optimize the \structure{acquisition function}: 194 | $$\theta_{n+1} = \arg \min_{\theta \in \Theta} \hat J(\theta) - \alert{\delta} z(\theta)$$ 195 | \end{enumerate} 196 | \column{.45\textwidth} 197 | \centering 198 | \begin{figure} 199 | Exploration function {\color{acq}$z(\theta)$} 200 | \includegraphics[width=.7\textwidth]{img/GLIS/acquisition.png} 201 | \end{figure} 202 | $\alert{\delta}$: exploitation vs.\\ exploration trade-off 203 | \end{columns} 204 | \vskip 1em 205 | to get the \structure{query point} $\theta_{n+1}$. Execute experiment with $\theta_{n+1}$, measure $J_{n+1}$. 206 | \pause 207 | \vskip 1em 208 | Iterate the procedure for $n+2, n+3 \dots$. 209 | \end{frame} 210 | 211 | \begin{frame}{MPC Calibration Example}%{Cart-pole system} 212 | \begin{columns} 213 | \column{.48\textwidth} 214 | \centering 215 | Cart-pole system 216 | \begin{figure} 217 | \includegraphics[width=.45\textwidth]{img/example/cart_pole.pdf} 218 | \end{figure} 219 | \begin{itemize} 220 | \item Input: force $F$ with \structure{fast disturbance} ($5~\text{rad/sec}$) 221 | \item Output: noisy position $p$%$ with white measurement noise 222 | \item Objective: follow trajectory for $p$, keep $\phi$ small. 223 | \end{itemize} 224 | \column{.54\textwidth} 225 | Optimization-based calibration of 226 | \begin{enumerate} 227 | \item MPC sample time $T_{s}^{\rm MPC}$ 228 | \item MPC weights $Q_y$ and $Q_{\Delta u}$ 229 | \item Prediction and control horizon 230 | \item Kalman filter covariance matrices 231 | \item QP solver's abs. and rel. tolerances 232 | \end{enumerate} 233 | 234 | \vskip 1em 235 | $n_\theta=14$ design parameters optimized using $n=500$ iterations of GLIS 236 | 237 | \begin{small} 238 | \begin{equation*} 239 | J = \int_0^{T_{\rm exp}} |\overline{p}(t) - p(t)| + 30 |\phi(t)|\;dt 240 | \end{equation*} 241 | \end{small} 242 | % \footnotesize 243 | % \url{https://github.com/forgi86/efficient-calibration-embedded-MPC.git} 244 | \end{columns} 245 | %\pause 246 | %\vskip 1em 247 | \pause 248 | Python codes on-line, $100\%$ \structure{open source} (including all dependencies).\\ 249 | \small 250 | \url{https://github.com/forgi86/efficient-calibration-embedded-MPC} 251 | \end{frame} 252 | 253 | \begin{frame}{MPC Calibration Example}%{Cart-pole system} 254 | \begin{columns} 255 | \column{.3\textwidth} 256 | \begin{figure} 257 | \centering 258 | %GLIS iterations on the PC 259 | \includegraphics[width=.99\textwidth]{img/example/HIL_PC.pdf} 260 | \end{figure} 261 | \column{.2\textwidth} 262 | \centering 263 | \huge vs. 264 | \column{.3\textwidth} 265 | \begin{figure} 266 | %GLIS iterations on the Raspberry PI 267 | \centering 268 | \includegraphics[width=.99\textwidth]{img/example/HIL_PI.pdf} 269 | \end{figure} 270 | \end{columns} 271 | \vskip 1em 272 | \begin{itemize} 273 | \item An Intel i5 PC (left) vs. an ARM-based Raspberry PI 3 (right) 274 | %\item On the same control problem, running the same Python code 275 | \item PI is about 10 time slower than the PC for MPC computations 276 | %\item Cart-pole simulated in the same Python code for simplicity \dots 277 | %\item Could be used in a true HIL setup or a real system 278 | \end{itemize} 279 | \end{frame} 280 | 281 | 282 | \begin{frame}{MPC Calibration Example}%{Cart-pole system} 283 | \begin{columns} 284 | \column{.5\textwidth} 285 | \begin{figure} 286 | \centering 287 | Optimized MPC on the PC 288 | \includegraphics[width=.99\textwidth]{img/example/BEST_GLIS_PC.pdf} 289 | $T_s^{\rm MPC} = 6$~ms 290 | \end{figure} 291 | \column{.5\textwidth} 292 | \begin{figure} 293 | Optimized MPC on the Raspberry PI 294 | \centering 295 | \includegraphics[width=.99\textwidth]{img/example/BEST_GLIS_PI.pdf} 296 | $T_s^{\rm MPC} = 22$~ms 297 | \end{figure} 298 | \end{columns} 299 | \vskip 1em 300 | \begin{itemize} 301 | \item Position and angle control tighter on the PC 302 | \item Faster loop update on the PC $\Rightarrow$ more effective disturbance rejection 303 | \item Calibration squeezes max performance out of the hardware! 304 | \end{itemize} 305 | \end{frame} 306 | 307 | \begin{frame}{MPC Calibration Example}%{Cart-pole system} 308 | \begin{columns} 309 | \column{.5\textwidth} 310 | \begin{figure} 311 | \centering 312 | GLIS iterations on the PC 313 | \includegraphics[width=.99\textwidth]{img/example/ITER_GLIS_PC_500.pdf} 314 | \end{figure} 315 | \column{.5\textwidth} 316 | \begin{figure} 317 | GLIS iterations on the Raspberry PI 318 | \centering 319 | \includegraphics[width=.99\textwidth]{img/example/ITER_GLIS_PI_500.pdf} 320 | \end{figure} 321 | \end{columns} 322 | \vskip 1em 323 | \begin{itemize} 324 | \item High cost for interrupted tests/unfeasible design 325 | \item For increasing $n$, more points have ``low'' cost 326 | \item Experiments with high cost still appear for large $n$ (exploration) 327 | %\item Optimum on PC is slightly better than on the PI. Some configurations are only feasible on PC! 328 | \end{itemize} 329 | \end{frame} 330 | 331 | \begin{frame}{Simulation Example}{Cart-pole system} 332 | Similar calibration results are obtained using Bayesian Optimization (BO).\\ 333 | However, BO is a much heavier algorithm! 334 | \vskip -.5em 335 | \begin{columns} 336 | \column{.5\textwidth} 337 | \begin{figure} 338 | \centering 339 | GLIS iterations on the PI 340 | \includegraphics[width=.99\textwidth]{img/example/COMPUTATION_GLIS_PI.pdf} 341 | \end{figure} 342 | \column{.5\textwidth} 343 | \begin{figure} 344 | BO iterations on the PI 345 | \centering 346 | \includegraphics[width=1\textwidth]{img/example/COMPUTATION_BO_PI.pdf} 347 | \end{figure} 348 | \end{columns} 349 | \begin{itemize} 350 | \item With GLIS, the overall computation time is dominated by function evaluations (= running MPC and simulating the system). 351 | %\item A lightweight algorithm like GLIS could be used for self-tuning on an embedded system! 352 | \end{itemize} 353 | \end{frame} 354 | 355 | \begin{frame}{Conclusions} 356 | An approach for embedded MPC calibration approach based on global optimization and repeated experiments 357 | \begin{itemize} 358 | \item Higher- and lower-level tuning knobs jointly optimized for performance, while keeping the design feasible for real-time implementation. 359 | \item Tested in simulation on two hardware platforms 360 | \end{itemize} 361 | \vskip 1em 362 | \pause 363 | Current/future works 364 | \begin{itemize} 365 | \item Application to {robotic systems} 366 | \item Preference-based MPC calibration 367 | \end{itemize} 368 | \end{frame} 369 | 370 | \begin{frame}{}{} 371 | \begin{center} 372 | \huge{\structure{Thank you.\\ Questions?}}\\ 373 | \vskip 1em 374 | \begin{small} 375 | \texttt{marco.forgione@idsia.ch} 376 | \end{small} 377 | \end{center} 378 | \end{frame} 379 | 380 | %\appendix 381 | %\newcounter{finalframe} 382 | %\setcounter{finalframe}{\value{framenumber}} 383 | %\setcounter{framenumber}{\value{finalframe}} 384 | 385 | \end{document} 386 | --------------------------------------------------------------------------------