-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathcontroller.py
287 lines (228 loc) · 12.5 KB
/
controller.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import os
import casadi as cs
import numpy as np
from acados_template import AcadosOcp, AcadosOcpSolver, AcadosModel
from quadrotor import Quadrotor3D
from utils import skew_symmetric, v_dot_q, quaternion_inverse
class Controller:
def __init__(self, quad:Quadrotor3D, t_horizon=1, n_nodes=20,
q_cost=None, r_cost=None, q_mask=None, rdrv_d_mat=None,
model_name="quad_3d_acados_mpc", solver_options=None):
"""
:param quad: quadrotor object
:type quad: Quadrotor3D
:param t_horizon: time horizon for MPC optimization
:param n_nodes: number of optimization nodes until time horizon
:param q_cost: diagonal of Q matrix for LQR cost of MPC cost function. Must be a numpy array of length 12.
:param r_cost: diagonal of R matrix for LQR cost of MPC cost function. Must be a numpy array of length 4.
:param q_mask: Optional boolean mask that determines which variables from the state compute towards the cost
function. In case no argument is passed, all variables are weighted.
:param solver_options: Optional set of extra options dictionary for solvers.
:param rdrv_d_mat: 3x3 matrix that corrects the drag with a linear model according to Faessler et al. 2018. None
if not used
"""
# Weighted squared error loss function q = (p_xyz, a_xyz, v_xyz, r_xyz), r = (u1, u2, u3, u4)
if q_cost is None:
q_cost = np.array([10, 10, 10, 0.1, 0.1, 0.1, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05])
if r_cost is None:
r_cost = np.array([0.1, 0.1, 0.1, 0.1])
self.T = t_horizon # Time horizon
self.N = n_nodes # number of control nodes within horizon
self.quad = quad
self.max_u = quad.max_input_value
self.min_u = quad.min_input_value
# Declare model variables
self.p = cs.MX.sym('p', 3) # position
self.q = cs.MX.sym('a', 4) # angle quaternion (wxyz)
self.v = cs.MX.sym('v', 3) # velocity
self.r = cs.MX.sym('r', 3) # angle rate
# Full state vector (13-dimensional)
self.x = cs.vertcat(self.p, self.q, self.v, self.r)
self.state_dim = 13
# Control input vector
u1 = cs.MX.sym('u1')
u2 = cs.MX.sym('u2')
u3 = cs.MX.sym('u3')
u4 = cs.MX.sym('u4')
self.u = cs.vertcat(u1, u2, u3, u4)
# Nominal model equations symbolic function (no GP)
self.quad_xdot_nominal = self.quad_dynamics(rdrv_d_mat)
# Initialize objective function, 0 target state and integration equations
self.L = None
self.target = None
# Build full model. Will have 13 variables. self.dyn_x contains the symbolic variable that
# should be used to evaluate the dynamics function. It corresponds to self.x if there are no GP's, or
# self.x_with_gp otherwise
acados_models, nominal_with_gp = self.acados_setup_model(
self.quad_xdot_nominal(x=self.x, u=self.u)['x_dot'], model_name)
# Convert dynamics variables to functions of the state and input vectors
self.quad_xdot = {}
for dyn_model_idx in nominal_with_gp.keys():
dyn = nominal_with_gp[dyn_model_idx]
self.quad_xdot[dyn_model_idx] = cs.Function('x_dot', [self.x, self.u], [dyn], ['x', 'u'], ['x_dot'])
# ### Setup and compile Acados OCP solvers ### #
self.acados_ocp_solver = {}
# Add one more weight to the rotation (use quaternion norm weighting in acados)
q_diagonal = np.concatenate((q_cost[:3], np.mean(q_cost[3:6])[np.newaxis], q_cost[3:]))
if q_mask is not None:
q_mask = np.concatenate((q_mask[:3], np.zeros(1), q_mask[3:]))
q_diagonal *= q_mask
for key, key_model in zip(acados_models.keys(), acados_models.values()):
nx = key_model.x.size()[0]
nu = key_model.u.size()[0]
ny = nx + nu
n_param = key_model.p.size()[0] if isinstance(key_model.p, cs.MX) else 0
# Create OCP object to formulate the optimization
ocp = AcadosOcp()
ocp.model = key_model
ocp.dims.N = self.N
ocp.solver_options.tf = t_horizon
# Initialize parameters
ocp.dims.np = n_param
ocp.parameter_values = np.zeros(n_param)
ocp.cost.cost_type = 'LINEAR_LS'
ocp.cost.cost_type_e = 'LINEAR_LS'
ocp.cost.W = np.diag(np.concatenate((q_diagonal, r_cost)))
ocp.cost.W_e = np.diag(q_diagonal)
terminal_cost = 0 if solver_options is None or not solver_options["terminal_cost"] else 1
ocp.cost.W_e *= terminal_cost
ocp.cost.Vx = np.zeros((ny, nx))
ocp.cost.Vx[:nx, :nx] = np.eye(nx)
ocp.cost.Vu = np.zeros((ny, nu))
ocp.cost.Vu[-4:, -4:] = np.eye(nu)
ocp.cost.Vx_e = np.eye(nx)
# Initial reference trajectory (will be overwritten)
x_ref = np.zeros(nx)
ocp.cost.yref = np.concatenate((x_ref, np.array([0.0, 0.0, 0.0, 0.0])))
ocp.cost.yref_e = x_ref
# Initial state (will be overwritten)
ocp.constraints.x0 = x_ref
# Set constraints
ocp.constraints.lbu = np.array([self.min_u] * 4)
ocp.constraints.ubu = np.array([self.max_u] * 4)
ocp.constraints.idxbu = np.array([0, 1, 2, 3])
# Solver options
ocp.solver_options.qp_solver = 'FULL_CONDENSING_HPIPM'
ocp.solver_options.hessian_approx = 'GAUSS_NEWTON'
ocp.solver_options.integrator_type = 'ERK'
ocp.solver_options.print_level = 0
ocp.solver_options.nlp_solver_type = 'SQP_RTI' if solver_options is None else solver_options["solver_type"]
# Compile acados OCP solver if necessary
json_file = os.path.join('./', key_model.name + '_acados_ocp.json')
self.acados_ocp_solver[key] = AcadosOcpSolver(ocp, json_file=json_file)
def acados_setup_model(self, nominal, model_name):
"""
Builds an Acados symbolic models using CasADi expressions.
:param model_name: name for the acados model. Must be different from previously used names or there may be
problems loading the right model.
:param nominal: CasADi symbolic nominal model of the quadrotor: f(self.x, self.u) = x_dot, dimensions 13x1.
:return: Returns a total of three outputs, where m is the number of GP's in the GP ensemble, or 1 if no GP:
- A dictionary of m AcadosModel of the GP-augmented quadrotor
- A dictionary of m CasADi symbolic nominal dynamics equations with GP mean value augmentations (if with GP)
:rtype: dict, dict, cs.MX
"""
def fill_in_acados_model(x, u, p, dynamics, name):
x_dot = cs.MX.sym('x_dot', dynamics.shape)
f_impl = x_dot - dynamics
# Dynamics model
model = AcadosModel()
model.f_expl_expr = dynamics
model.f_impl_expr = f_impl
model.x = x
model.xdot = x_dot
model.u = u
model.p = p
model.name = name
return model
acados_models = {}
dynamics_equations = {}
# No available GP so return nominal dynamics
dynamics_equations[0] = nominal
x_ = self.x
dynamics_ = nominal
acados_models[0] = fill_in_acados_model(x=x_, u=self.u, p=[], dynamics=dynamics_, name=model_name)
return acados_models, dynamics_equations
def quad_dynamics(self, rdrv_d):
"""
Symbolic dynamics of the 3D quadrotor model. The state consists on: [p_xyz, a_wxyz, v_xyz, r_xyz]^T, where p
stands for position, a for angle (in quaternion form), v for velocity and r for body rate. The input of the
system is: [u_1, u_2, u_3, u_4], i.e. the activation of the four thrusters.
:param rdrv_d: a 3x3 diagonal matrix containing the D matrix coefficients for a linear drag model as proposed
by Faessler et al.
:return: CasADi function that computes the analytical differential state dynamics of the quadrotor model.
Inputs: 'x' state of quadrotor (6x1) and 'u' control input (2x1). Output: differential state vector 'x_dot'
(6x1)
"""
x_dot = cs.vertcat(self.p_dynamics(), self.q_dynamics(), self.v_dynamics(rdrv_d), self.w_dynamics())
return cs.Function('x_dot', [self.x[:13], self.u], [x_dot], ['x', 'u'], ['x_dot'])
def p_dynamics(self):
return self.v
def q_dynamics(self):
return 1 / 2 * cs.mtimes(skew_symmetric(self.r), self.q)
def v_dynamics(self, rdrv_d):
"""
:param rdrv_d: a 3x3 diagonal matrix containing the D matrix coefficients for a linear drag model as proposed
by Faessler et al. None, if no linear compensation is to be used.
"""
f_thrust = self.u * self.quad.max_thrust
g = cs.vertcat(0.0, 0.0, 9.81)
a_thrust = cs.vertcat(0.0, 0.0, f_thrust[0] + f_thrust[1] + f_thrust[2] + f_thrust[3]) / self.quad.mass
v_dynamics = v_dot_q(a_thrust, self.q) - g
if rdrv_d is not None:
# Velocity in body frame:
v_b = v_dot_q(self.v, quaternion_inverse(self.q))
rdrv_drag = v_dot_q(cs.mtimes(rdrv_d, v_b), self.q)
v_dynamics += rdrv_drag
return v_dynamics
def w_dynamics(self):
f_thrust = self.u * self.quad.max_thrust
y_f = cs.MX(self.quad.y_f)
x_f = cs.MX(self.quad.x_f)
c_f = cs.MX(self.quad.z_l_tau)
return cs.vertcat(
(cs.mtimes(f_thrust.T, y_f) + (self.quad.J[1] - self.quad.J[2]) * self.r[1] * self.r[2]) / self.quad.J[0],
(-cs.mtimes(f_thrust.T, x_f) + (self.quad.J[2] - self.quad.J[0]) * self.r[2] * self.r[0]) / self.quad.J[1],
(cs.mtimes(f_thrust.T, c_f) + (self.quad.J[0] - self.quad.J[1]) * self.r[0] * self.r[1]) / self.quad.J[2])
def run_optimization(self, initial_state=None, goal=None, use_model=0, return_x=False, mode='pose'):
"""
Optimizes a trajectory to reach the pre-set target state, starting from the input initial state, that minimizes
the quadratic cost function and respects the constraints of the system
:param initial_state: 13-element list of the initial state. If None, 0 state will be used
:param goal: 3 element [x,y,z] for moving to goal mode, 3*(N+1) for trajectory tracking mode
:param use_model: integer, select which model to use from the available options.
:param return_x: bool, whether to also return the optimized sequence of states alongside with the controls.
:param mode: string, whether to use moving to pose mode or tracking mode
:return: optimized control input sequence (flattened)
"""
if initial_state is None:
initial_state = [0, 0, 0] + [1, 0, 0, 0] + [0, 0, 0] + [0, 0, 0]
# Set initial state
x_init = initial_state
x_init = np.stack(x_init)
# Set initial condition, equality constraint
self.acados_ocp_solver[use_model].set(0, 'lbx', x_init)
self.acados_ocp_solver[use_model].set(0, 'ubx', x_init)
# Set final condition
if mode == "pose":
for j in range(self.N):
y_ref = np.array([goal[0], goal[1], goal[2], 1,0,0,0, 0,0,0, 0,0,0, 0,0,0,0])
self.acados_ocp_solver[use_model].set(j, 'yref', y_ref)
y_refN = np.array([goal[0], goal[1], goal[2], 1,0,0,0, 0,0,0, 0,0,0])
self.acados_ocp_solver[use_model].set(self.N, 'yref', y_refN)
else:
for j in range(self.N):
y_ref = np.array([goal[j,0], goal[j,1], goal[j,2], 1,0,0,0, 0,0,0, 0,0,0, 0,0,0,0])
self.acados_ocp_solver[use_model].set(j, 'yref', y_ref)
y_refN = np.array([goal[self.N,0], goal[self.N,1], goal[self.N,2], 1,0,0,0, 0,0,0, 0,0,0])
self.acados_ocp_solver[use_model].set(self.N, 'yref', y_refN)
# Solve OCP
self.acados_ocp_solver[use_model].solve()
# Get u
w_opt_acados = np.ndarray((self.N, 4))
x_opt_acados = np.ndarray((self.N + 1, len(x_init)))
x_opt_acados[0, :] = self.acados_ocp_solver[use_model].get(0, "x")
for i in range(self.N):
w_opt_acados[i, :] = self.acados_ocp_solver[use_model].get(i, "u")
x_opt_acados[i + 1, :] = self.acados_ocp_solver[use_model].get(i + 1, "x")
w_opt_acados = np.reshape(w_opt_acados, (-1))
return w_opt_acados if not return_x else (w_opt_acados, x_opt_acados)