19
19
from casadi import MX
20
20
from numpy .typing import NDArray
21
21
22
- from crazyflow .constants import GRAVITY
22
+ from crazyflow .constants import ARM_LEN , GRAVITY , SIGN_MIX_MATRIX
23
+ from crazyflow .control .control import KF , KM , Control
23
24
from crazyflow .sim import Sim
24
25
25
26
@@ -142,9 +143,11 @@ def setup_linearization(self):
142
143
self .loss = cs .Function ("loss" , l_inputs , l_outputs , l_inputs_str , l_outputs_str )
143
144
144
145
145
- def symbolic ( mass : float , J : NDArray , dt : float ) -> SymbolicModel :
146
+ def symbolic_attitude ( dt : float ) -> SymbolicModel :
146
147
"""Create symbolic (CasADi) models for dynamics, observation, and cost of a quadcopter.
147
148
149
+ This model is based on the identified model derived from real-world data of the Crazyflie 2.1.
150
+
148
151
Returns:
149
152
The CasADi symbolic model of the environment.
150
153
"""
@@ -177,13 +180,15 @@ def symbolic(mass: float, J: NDArray, dt: float) -> SymbolicModel:
177
180
params_acc = [20.907574256269616 , 3.653687545690674 ]
178
181
params_roll_rate = [- 130.3 , - 16.33 , 119.3 ]
179
182
params_pitch_rate = [- 99.94 , - 13.3 , 84.73 ]
180
- # params_yaw_rate = [0, 0, 0], because we always keep yaw as 0 when we identified the parameters.
181
- # We introduce a small negative offset here to make sure that we could get result of LQR ect..
182
- # TODO: identify params_yaw_rate
183
+ # The identified model sets params_yaw_rate to [0, 0, 0], because the training data did not
184
+ # contain any data with yaw != 0. Therefore, it cannot infer the impact of setting the yaw
185
+ # attitude to a non-zero value on the dynamics. However, using a zero vector will make the
186
+ # system matrix ill-conditioned for control methods like LQR. Therefore, we introduce a small
187
+ # spring-like term to the yaw dynamics that leads to a non-singular system matrix.
188
+ # TODO: identify proper parameters for yaw_rate from real data.
183
189
params_yaw_rate = [- 0.01 , 0 , 0 ]
184
190
185
191
# Define dynamics equations.
186
- # TODO: create a parameter for the new quad model
187
192
X_dot = cs .vertcat (
188
193
x_dot ,
189
194
(params_acc [0 ] * T + params_acc [1 ])
@@ -213,6 +218,76 @@ def symbolic(mass: float, J: NDArray, dt: float) -> SymbolicModel:
213
218
return SymbolicModel (dynamics = dynamics , cost = cost , dt = dt )
214
219
215
220
221
+ def symbolic_thrust (mass : float , J : NDArray , dt : float ) -> SymbolicModel :
222
+ """Create symbolic (CasADi) models for dynamics, observation, and cost of a quadcopter.
223
+
224
+ This model is based on the analytical model of Luis, Carlos, and Jérôme Le Ny. "Design of a
225
+ trajectory tracking controller for a nanoquadcopter." arXiv preprint arXiv:1608.05786 (2016).
226
+
227
+ Returns:
228
+ The CasADi symbolic model of the environment.
229
+ """
230
+ # Define states.
231
+ z = MX .sym ("z" )
232
+ z_dot = MX .sym ("z_dot" )
233
+
234
+ # Set up the dynamics model for a 3D quadrotor.
235
+ nx , nu = 12 , 4
236
+ Ixx , Iyy , Izz = J .diagonal ()
237
+ J = cs .blockcat ([[Ixx , 0.0 , 0.0 ], [0.0 , Iyy , 0.0 ], [0.0 , 0.0 , Izz ]])
238
+ Jinv = cs .blockcat ([[1.0 / Ixx , 0.0 , 0.0 ], [0.0 , 1.0 / Iyy , 0.0 ], [0.0 , 0.0 , 1.0 / Izz ]])
239
+ gamma = KM / KF
240
+ # System state variables
241
+ x , y = MX .sym ("x" ), MX .sym ("y" )
242
+ x_dot , y_dot = MX .sym ("x_dot" ), MX .sym ("y_dot" )
243
+ phi , theta , psi = MX .sym ("phi" ), MX .sym ("theta" ), MX .sym ("psi" )
244
+ p , q , r = MX .sym ("p" ), MX .sym ("q" ), MX .sym ("r" )
245
+ # Rotation matrix transforming a vector in the body frame to the world frame. PyBullet Euler
246
+ # angles use the SDFormat for rotation matrices.
247
+ Rob = csRotXYZ (phi , theta , psi )
248
+ # Define state variables.
249
+ X = cs .vertcat (x , x_dot , y , y_dot , z , z_dot , phi , theta , psi , p , q , r )
250
+ # Define inputs.
251
+ f1 , f2 , f3 , f4 = MX .sym ("f1" ), MX .sym ("f2" ), MX .sym ("f3" ), MX .sym ("f4" )
252
+ U = cs .vertcat (f1 , f2 , f3 , f4 )
253
+
254
+ # Defining the dynamics function.
255
+ # We are using the velocity of the base wrt to the world frame expressed in the world frame.
256
+ # Note that the reference expresses this in the body frame.
257
+ oVdot_cg_o = Rob @ cs .vertcat (0 , 0 , f1 + f2 + f3 + f4 ) / mass - cs .vertcat (0 , 0 , GRAVITY )
258
+ pos_ddot = oVdot_cg_o
259
+ pos_dot = cs .vertcat (x_dot , y_dot , z_dot )
260
+ # We use the spin directions (signs) from the mix matrix used in the simulation.
261
+ sx , sy , sz = SIGN_MIX_MATRIX [..., 0 ], SIGN_MIX_MATRIX [..., 1 ], SIGN_MIX_MATRIX [..., 2 ]
262
+ Mb = cs .vertcat (
263
+ ARM_LEN / cs .sqrt (2.0 ) * (sx [0 ] * f1 + sx [1 ] * f2 + sx [2 ] * f3 + sx [3 ] * f4 ),
264
+ ARM_LEN / cs .sqrt (2.0 ) * (sy [0 ] * f1 + sy [1 ] * f2 + sy [2 ] * f3 + sy [3 ] * f4 ),
265
+ gamma * (sz [0 ] * f1 + sz [1 ] * f2 + sz [2 ] * f3 + sz [3 ] * f4 ),
266
+ )
267
+ rate_dot = Jinv @ (Mb - (cs .skew (cs .vertcat (p , q , r )) @ J @ cs .vertcat (p , q , r )))
268
+ ang_dot = cs .blockcat (
269
+ [
270
+ [1 , cs .sin (phi ) * cs .tan (theta ), cs .cos (phi ) * cs .tan (theta )],
271
+ [0 , cs .cos (phi ), - cs .sin (phi )],
272
+ [0 , cs .sin (phi ) / cs .cos (theta ), cs .cos (phi ) / cs .cos (theta )],
273
+ ]
274
+ ) @ cs .vertcat (p , q , r )
275
+ X_dot = cs .vertcat (
276
+ pos_dot [0 ], pos_ddot [0 ], pos_dot [1 ], pos_ddot [1 ], pos_dot [2 ], pos_ddot [2 ], ang_dot , rate_dot
277
+ )
278
+
279
+ Y = cs .vertcat (x , x_dot , y , y_dot , z , z_dot , phi , theta , psi , p , q , r )
280
+
281
+ # Define cost (quadratic form).
282
+ Q , R = MX .sym ("Q" , nx , nx ), MX .sym ("R" , nu , nu )
283
+ Xr , Ur = MX .sym ("Xr" , nx , 1 ), MX .sym ("Ur" , nu , 1 )
284
+ cost_func = 0.5 * (X - Xr ).T @ Q @ (X - Xr ) + 0.5 * (U - Ur ).T @ R @ (U - Ur )
285
+ # Define dynamics and cost dictionaries.
286
+ dynamics = {"dyn_eqn" : X_dot , "obs_eqn" : Y , "vars" : {"X" : X , "U" : U }}
287
+ cost = {"cost_func" : cost_func , "vars" : {"X" : X , "U" : U , "Xr" : Xr , "Ur" : Ur , "Q" : Q , "R" : R }}
288
+ return SymbolicModel (dynamics = dynamics , cost = cost , dt = dt )
289
+
290
+
216
291
def symbolic_from_sim (sim : Sim ) -> SymbolicModel :
217
292
"""Create a symbolic model from a Sim instance.
218
293
@@ -226,8 +301,14 @@ def symbolic_from_sim(sim: Sim) -> SymbolicModel:
226
301
The model is expected to deviate from the true dynamics when the sim parameters are
227
302
randomized.
228
303
"""
229
- mass , J = sim .default_data .params .mass [0 , 0 ], sim .default_data .params .J [0 , 0 ]
230
- return symbolic (mass , J , 1 / sim .freq )
304
+ match sim .control :
305
+ case Control .attitude :
306
+ return symbolic_attitude (1 / sim .freq )
307
+ case Control .thrust :
308
+ mass , J = sim .default_data .params .mass [0 , 0 ], sim .default_data .params .J [0 , 0 ]
309
+ return symbolic_thrust (mass , J , 1 / sim .freq )
310
+ case _:
311
+ raise ValueError (f"Unsupported control type for symbolic model: { sim .control } " )
231
312
232
313
233
314
def csRotXYZ (phi : float , theta : float , psi : float ) -> MX :
0 commit comments