Hi everyone!
I am writing an NMPC solver to solve nonlinear MPC problems using a Python interface. Since the cost function is not in a least squares (LS) form, I use the “EXTERNAL” type to define my cost function. However, I encounter some issues when setting up the solver.
Here is my model:
Code
model_name = 'mpcc_ode'
# parameters
mass = 0.9 # [kg]
g0 = 9.8015 # [m/s^2]
arm_L = 0.16 # [m]
cm = 4.888486266072161e-06
ct = 0.000367717
cq = cm/ct
Ixx = 0.0046890742
Iyy = 0.0046890742
Izz = 0.010421166
inv_Ixx = 1/Ixx
inv_Iyy = 1/Iyy
inv_Izz = 1/Izz
M_PI = np.pi
# set up states & controls
p = SX.sym('p', 3)
v = SX.sym('v', 3)
q = SX.sym('q', 4)
w = SX.sym('w', 3)
f = SX.sym('f', 4)
theta = SX.sym('theta')
v_theta = SX.sym('v_theta')
x = vertcat(p, v, q, w, f, theta, v_theta)
df = SX.sym('df', 4)
dv_theta = SX.sym('dv_theta')
u = vertcat(df, dv_theta)
# xdot
p_dot = SX.sym('p_dot', 3)
v_dot = SX.sym('v_dot', 3)
q_dot = SX.sym('q_dot', 4)
w_dot = SX.sym('w_dot', 3)
f_dot = SX.sym('f_dot', 4)
theta_dot = SX.sym('theta_dot')
v_theta_dot = SX.sym('v_theta_dot')
xdot = vertcat(p_dot, v_dot, q_dot, w_dot, f_dot, theta_dot, v_theta_dot)
# dynamics
f_expl = vertcat(v[0],
v[1],
v[2],
-2*(q[0]*q[2]+q[1]*q[3])*(f[0]+f[1]+f[2]+f[3])/mass,
-2*(q[2]*q[3]-q[0]*q[1])*(f[0]+f[1]+f[2]+f[3])/mass,
g0-(q[0]*q[0]-q[1]*q[1]-q[2]*q[2]+q[3]*q[3])*(f[0]+f[1]+f[2]+f[3])/mass,
0.5*(-q[1]*w[0]-q[2]*w[1]-q[3]*w[2]),
0.5*(q[0]*w[0]+q[2]*w[2]-q[3]*w[1]),
0.5*(q[0]*w[1]-q[1]*w[2]+q[3]*w[0]),
0.5*(q[0]*w[2]+q[1]*w[1]-q[2]*w[0]),
inv_Ixx*(Iyy-Izz)*w[1]*w[2]+inv_Ixx*arm_L*cos(M_PI/4)*(-f[0]+f[1]+f[2]-f[3]),
inv_Iyy*(Izz-Ixx)*w[0]*w[2]+inv_Iyy*arm_L*cos(M_PI/4)*(f[0]-f[1]+f[2]-f[3]),
inv_Izz*(Ixx-Iyy)*w[0]*w[1]+inv_Izz*cq*(f[0]+f[1]-f[2]-f[3]),
df[0],
df[1],
df[2],
df[3],
v_theta,
dv_theta
)
f_impl = xdot - f_expl
# algebraic variables
z = []
# parameters
p = []
model = AcadosModel()
model.f_impl_expr = f_impl
model.f_expl_expr = f_expl
model.x = x
model.xdot = xdot
model.u = u
model.z = z
model.p = p
model.name = model_name
The cost function is given by:
First question:
In this cost function, the value q_c is a dynamic parameter, meaning that I need to change it each time I use this solver to solve an optimization problem.
To modify q_c , I set it as a dynamic parameter while keeping other cost weights fixed using the following code:
Code
# set dynamic cost weight q_c
self.cost_error_contour_param = ca.SX.sym('cost_error_contour')
self.ocp.model.p = ca.vertcat(self.cost_error_contour_param)
self.ocp.dims.np = 1
self.ocp.parameter_values = np.array([500])
Then, I define part of the q_c cost function as:
Code
p_x, p_y, p_z = self.model.x[0], self.model.x[1], self.model.x[2]
theta = self.model.x[-2]
p_theta_x_ref = 0.57735 * theta
p_theta_y_ref = 0.57735 * theta
p_theta_z_ref = 0.57735 * theta
error_pos_x = p_x - p_theta_x_ref
error_pos_y = p_y - p_theta_y_ref
error_pos_z = p_z - p_theta_z_ref
v_theta_x_ref = 0.57735
v_theta_y_ref = 0.57735
v_theta_z_ref = 0.57735
self.ocp.cost.cost_type = "EXTERNAL"
self.ocp.cost.cost_type_e = "EXTERNAL"
error_contour_x = (1 - v_theta_x_ref**2) * error_pos_x - (v_theta_x_ref * v_theta_y_ref) * error_pos_y - (v_theta_x_ref * v_theta_z_ref) * error_pos_z
error_contour_y = (1 - v_theta_y_ref**2) * error_pos_y - (v_theta_y_ref * v_theta_x_ref) * error_pos_x - (v_theta_y_ref * v_theta_z_ref) * error_pos_z
error_contour_z = (1 - v_theta_z_ref**2) * error_pos_z - (v_theta_x_ref * v_theta_z_ref) * error_pos_x - (v_theta_y_ref * v_theta_z_ref) * error_pos_y
cost_error_contour = ca.sum2(self.ocp.model.p[0] * error_contour_x
+ self.ocp.model.p[0] * error_contour_y
+ self.ocp.model.p[0] * error_contour_z)
Other cost functions are omitted here. The final cost function is:
Code
cost_expr = cost_error_lag + cost_error_contour + cost_error_w + cost_error_df + cost_error_dv_theta + cost_error_v
self.ocp.model.cost_expr_ext_cost = ca.sum2(cost_expr)
self.ocp.model.cost_expr_ext_cost_e = ca.SX(0)
self.acados_solver = AcadosOcpSolver(self.ocp, json_file = 'acados_ocp.json')
However, when I use acados_solver
in the main function to solve the NMPC problem, I attempt to modify self.ocp.model.p[0]
using:
Code
# Init solver
mpcc_controller = MPCC_Controller()
# change q_c
mpcc_controller.acados_solver.set(0, "p", 200.0)
# solve NMPC question
_dt, w = mpcc_controller.mpcc_control(current_state)
It seems that the solution does not update when I modify self.ocp.model.p[0]
using mpcc_controller.acados_solver.set(0, "p", 200.0)
, even when I change the value to 0.0
or 5000.0
. However, if I modify the value in self.ocp.parameter_values = np.array([500])
, the solution changes.
Are there any errors in my code, or is there another way to correctly modify this parameter? Should I consider using the “NONLINEAR_LS” cost function instead?
Second question:
Another question is that variable \hat{e}^{l}\left(\hat{\theta}_{k}\right) in the cost function is computed as
\hat{e}^{l}\left(\hat{\theta}_{k}\right) = p_x\cdot p\left ( {\theta}_{k} \right )_x + p_y\cdot p\left ( {\theta}_{k} \right )_y +p_z\cdot p\left ( {\theta}_{k} \right )_z
where p_x, p_y, p_z and \theta_k are state variables in this NMPC question, p\left ( {\theta}_{k} \right )_x , p\left ( {\theta}_{k} \right )_y and p\left ( {\theta}_{k} \right )_z can be calculated by:
p\left ( {\theta}_{k} \right )_x = a_{0,x} + a_{1,x} \cdot {\theta}_{k} + a_{2,x} \cdot {\theta}_{k}^2 + a_{3,x} \cdot {\theta}_{k}^3
p\left ( {\theta}_{k} \right )_y = a_{0,y} + a_{1,y} \cdot {\theta}_{k} + a_{2,y} \cdot {\theta}_{k}^2 + a_{3,y} \cdot {\theta}_{k}^3
p\left ( {\theta}_{k} \right )_z= a_{0,z} + a_{1,z} \cdot {\theta}_{k} + a_{2,z} \cdot {\theta}_{k}^2 + a_{3,z} \cdot {\theta}_{k}^3
The polynomial coefficients a_{i,*} are dynamically updated.
Similarly, I encounter the same issue when trying to dynamically modify these 12 coefficients. Additionally, if the prediction step size is 20, the first 10 steps share one set of polynomial coefficients, while the last 10 steps have a different set. How can I set these coefficients properly when creating the solver?
Total Python Codes Here:
mpcc_ocp.py:
Code
from acados_template import AcadosOcp, AcadosOcpSolver
from mpcc_model import export_mpcc_ode_model
import numpy as np
import casadi as ca
import scipy.linalg
from os.path import dirname, join, abspath
import time
class MPCC_Controller:
def __init__(self, cost_params=None):
self.ocp = AcadosOcp() # OCP
self.model = export_mpcc_ode_model()
self.ocp.model = self.model # model set
# parameters
default_cost_params = {
'cost_error_lag': 1000.0,
'cost_error_w': 1.0,
'cost_error_df': 1.0,
'cost_error_dv_theta': 10.0,
'cost_error_v': 500.0,
'N': 21,
'Tf': 1.05
}
self.cost_params = {**default_cost_params, **(cost_params or {})}
# set dynamic q_c
self.cost_error_contour_param = ca.SX.sym('cost_error_contour')
self.ocp.model.p = ca.vertcat(self.cost_error_contour_param)
self.ocp.dims.np = 1
self.ocp.parameter_values = np.array([500.0])
self.nx = self.model.x.size()[0] # dimension of state
self.nu = self.model.u.size()[0] # dimension of control
self.ny = self.nx + self.nu # dimension of control + state
self.ny_e = self.nx # end dimension of control + state
# prediction steps and total prediction time
self.ocp.solver_options.N_horizon = self.cost_params['N']
self.ocp.dims.N = self.cost_params['N']
self.ocp.solver_options.tf = self.cost_params['Tf']
p_x, p_y, p_z = self.model.x[0], self.model.x[1], self.model.x[2]
theta = self.model.x[-2]
p_theta_x_ref = 0.57735 * theta
p_theta_y_ref = 0.57735 * theta
p_theta_z_ref = 0.57735 * theta
error_pos_x = p_x - p_theta_x_ref
error_pos_y = p_y - p_theta_y_ref
error_pos_z = p_z - p_theta_z_ref
v_theta_x_ref = 0.57735
v_theta_y_ref = 0.57735
v_theta_z_ref = 0.57735
self.ocp.cost.cost_type = "EXTERNAL"
self.ocp.cost.cost_type_e = "EXTERNAL"
# cost function for q_l
cost_error_lag = self.cost_params['cost_error_lag'] * (ca.dot(v_theta_x_ref, error_pos_x) + ca.dot(v_theta_y_ref, error_pos_y) + ca.dot(v_theta_z_ref, error_pos_z))
print(f"cost_error_lag shape: {cost_error_lag.shape}")
error_contour_x = (1 - v_theta_x_ref**2) * error_pos_x - (v_theta_x_ref * v_theta_y_ref) * error_pos_y - (v_theta_x_ref * v_theta_z_ref) * error_pos_z
error_contour_y = (1 - v_theta_y_ref**2) * error_pos_y - (v_theta_y_ref * v_theta_x_ref) * error_pos_x - (v_theta_y_ref * v_theta_z_ref) * error_pos_z
error_contour_z = (1 - v_theta_z_ref**2) * error_pos_z - (v_theta_x_ref * v_theta_z_ref) * error_pos_x - (v_theta_y_ref * v_theta_z_ref) * error_pos_y
print(f"error_contour_x shape: {error_contour_x.shape}")
error_w_x = self.model.x[10]**2
error_w_y = self.model.x[11]**2
error_w_z = self.model.x[12]**2
print(f"error_w_x shape: {error_w_x.shape}")
error_df_0 = self.model.u[0]**2
error_df_1 = self.model.u[1]**2
error_df_2 = self.model.u[2]**2
error_df_3 = self.model.u[3]**2
print(f"error_df_0 shape: {error_df_0.shape}")
error_dv_theta = self.model.u[4]**2
print(f"error_dv_theta shape: {error_dv_theta.shape}")
error_v = self.model.x[13]**2
print(f"error_v shape: {error_v.shape}")
# cost function for q_c
cost_error_contour = ca.sum2(self.ocp.model.p[0] * error_contour_x
+ self.ocp.model.p[0] * error_contour_y
+ self.ocp.model.p[0] * error_contour_z)
print(f"cost_error_contour shape: {cost_error_contour.shape}")
# cost function for q_w
cost_error_w = ca.sum2(self.cost_params['cost_error_w'] * error_w_x
+ self.cost_params['cost_error_w'] * error_w_y
+ self.cost_params['cost_error_w'] * error_w_z)
print(f"cost_error_w shape: {cost_error_w.shape}")
# cost function for R_df
cost_error_df = ca.sum2(self.cost_params['cost_error_df'] * error_df_0
+ self.cost_params['cost_error_df'] * error_df_1
+ self.cost_params['cost_error_df'] * error_df_2
+ self.cost_params['cost_error_df'] * error_df_3)
print(f"cost_error_df shape: {cost_error_df.shape}")
# cost function for R_dv_theta
cost_error_dv_theta = ca.sum2(self.cost_params['cost_error_dv_theta'] * error_dv_theta)
print(f"cost_error_dv_theta shape: {cost_error_dv_theta.shape}")
# cost function for R_v
cost_error_v = ca.sum2(self.cost_params['cost_error_v'] * error_v)
print(f"cost_error_v shape: {cost_error_v.shape}")
# total cost function
cost_expr = cost_error_lag + cost_error_contour + cost_error_w + cost_error_df + cost_error_dv_theta + cost_error_v
print(f"cost_expr type: {type(cost_expr)}, shape: {cost_expr.shape}")
# set cost function and end cost function
self.ocp.model.cost_expr_ext_cost = ca.sum2(cost_expr)
self.ocp.model.cost_expr_ext_cost_e = ca.SX(0)
# set constraints
self.ocp.constraints.lbu = np.array([-20, -20, -20, -20, 0])
self.ocp.constraints.ubu = np.array([20, 20, 20, 20, 50])
self.ocp.constraints.x0 = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) # 初始状态
self.ocp.constraints.idxbu = np.array([0, 1, 2, 3, 4])
self.ocp.constraints.lbx = np.array([-3.14, -3.14, -3.14, 0.01, 0.01, 0.01, 0.01, 0])
self.ocp.constraints.ubx = np.array([3.14, 3.14, 3.14, 20, 20, 20, 20, 20])
self.ocp.constraints.idxbx = np.array([10, 11, 12, 13, 14, 15, 16, 18])
self.ocp.constraints.x0 = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) # 初始状态
# set solver parameters
# self.ocp.solver_options.qp_solver = 'FULL_CONDENSING_QPOASES'
self.ocp.solver_options.qp_solver = 'FULL_CONDENSING_HPIPM'
# self.ocp.solver_options.qp_solver = 'PARTIAL_CONDENSING_HPIPM'
self.ocp.solver_options.hessian_approx = 'EXACT'
self.ocp.solver_options.integrator_type = 'ERK'
self.ocp.solver_options.qp_solver_iter_max = 5000
self.ocp.solver_options.print_level = 0
self.ocp.solver_options.nlp_solver_type = 'SQP_RTI'
# create ocp solver
self.acados_solver = AcadosOcpSolver(self.ocp, json_file = 'acados_ocp.json')
self.acados_solver.set(0, "p", np.array([self.ocp.parameter_values[0]]))
print("NMPC Controller Init Done")
def mpcc_control(self, current_state):
_start = time.perf_counter()
# set initial state
self.acados_solver.set(0, 'lbx', current_state)
self.acados_solver.set(0, 'ubx', current_state)
# solve the optimization problem
self.acados_solver.solve()
# print the solution
w_opt_acados = np.ndarray((self.cost_params['N'], 5)) # input
x_opt_acados = np.ndarray((self.cost_params['N'] + 1, len(current_state))) # state
x_opt_acados[0, :] = self.acados_solver.get(0, "x")
for i in range(self.cost_params['N']):
w_opt_acados[i, :] = self.acados_solver.get(i, "u")
x_opt_acados[i + 1, :] = self.acados_solver.get(i + 1, "x")
# return w_opt_acados, x_opt_acados
_end = time.perf_counter()
_dt = (_end - _start) * 1000 # change time to ms
print(f"Time taken to run mpcc_control: {_dt:.3f} ms")
return _dt, w_opt_acados[0] # return the first control input
def update_cost_countor_params(self, new_cost_error_contour):
"""
change cost_error_contour
"""
self.acados_solver.set(0, "p", np.array([new_cost_error_contour]))
if __name__ == '__main__':
print("ACADOS MPCC TEST")
# solver init
mpcc_controller = MPCC_Controller()
print(mpcc_controller.acados_solver.get(0, "p")[0])
# set initial state
current_state = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
# change q_c
mpcc_controller.acados_solver.reset()
mpcc_controller.acados_solver.set(0, "p", 500.0)
print(mpcc_controller.acados_solver.get(0, "p")[0])
# solve the optimization problem
_dt, w = mpcc_controller.mpcc_control(current_state)
print("Control Input:")
print(w)
mpcc_model.py:
Code
from acados_template import AcadosModel
from casadi import SX, vertcat, sin, cos
import numpy as np
def export_mpcc_ode_model():
model_name = 'mpcc_ode'
# parameters
mass = 0.9 # [kg]
g0 = 9.8015 # [m/s^2]
arm_L = 0.16 # [m]
cm = 4.888486266072161e-06
ct = 0.000367717
cq = cm/ct
Ixx = 0.0046890742
Iyy = 0.0046890742
Izz = 0.010421166
inv_Ixx = 1/Ixx
inv_Iyy = 1/Iyy
inv_Izz = 1/Izz
M_PI = np.pi
# set up states & controls
p = SX.sym('p', 3)
v = SX.sym('v', 3)
q = SX.sym('q', 4)
w = SX.sym('w', 3)
f = SX.sym('f', 4)
theta = SX.sym('theta')
v_theta = SX.sym('v_theta')
x = vertcat(p, v, q, w, f, theta, v_theta)
df = SX.sym('df', 4)
dv_theta = SX.sym('dv_theta')
u = vertcat(df, dv_theta)
# xdot
p_dot = SX.sym('p_dot', 3)
v_dot = SX.sym('v_dot', 3)
q_dot = SX.sym('q_dot', 4)
w_dot = SX.sym('w_dot', 3)
f_dot = SX.sym('f_dot', 4)
theta_dot = SX.sym('theta_dot')
v_theta_dot = SX.sym('v_theta_dot')
xdot = vertcat(p_dot, v_dot, q_dot, w_dot, f_dot, theta_dot, v_theta_dot)
# dynamics
f_expl = vertcat(v[0],
v[1],
v[2],
-2*(q[0]*q[2]+q[1]*q[3])*(f[0]+f[1]+f[2]+f[3])/mass,
-2*(q[2]*q[3]-q[0]*q[1])*(f[0]+f[1]+f[2]+f[3])/mass,
g0-(q[0]*q[0]-q[1]*q[1]-q[2]*q[2]+q[3]*q[3])*(f[0]+f[1]+f[2]+f[3])/mass,
0.5*(-q[1]*w[0]-q[2]*w[1]-q[3]*w[2]),
0.5*(q[0]*w[0]+q[2]*w[2]-q[3]*w[1]),
0.5*(q[0]*w[1]-q[1]*w[2]+q[3]*w[0]),
0.5*(q[0]*w[2]+q[1]*w[1]-q[2]*w[0]),
inv_Ixx*(Iyy-Izz)*w[1]*w[2]+inv_Ixx*arm_L*cos(M_PI/4)*(-f[0]+f[1]+f[2]-f[3]),
inv_Iyy*(Izz-Ixx)*w[0]*w[2]+inv_Iyy*arm_L*cos(M_PI/4)*(f[0]-f[1]+f[2]-f[3]),
inv_Izz*(Ixx-Iyy)*w[0]*w[1]+inv_Izz*cq*(f[0]+f[1]-f[2]-f[3]),
df[0],
df[1],
df[2],
df[3],
v_theta,
dv_theta
)
f_impl = xdot - f_expl
# algebraic variables
z = []
# parameters
p = []
model = AcadosModel()
model.f_impl_expr = f_impl
model.f_expl_expr = f_expl
model.x = x
model.xdot = xdot
model.u = u
model.z = z
model.p = p
model.name = model_name
# store meta information
model.x_labels = ['$p$ [m]', '$v$ [m/s]', '$q$', '$w$ [rad/s]', '$f$ [N]', '$\theta$ [m]', '$v_{\theta}$ [m/s]']
model.u_labels = ['$df$ [F/s]', '$dv_{\theta}$ [m/s^2]']
model.t_label = '$t$ [s]'
return model
Thanks for your advice!