|
| 1 | +"""Symbolic model and utilities for drone dynamics and control. |
| 2 | +
|
| 3 | +This module provides functionality to create symbolic representations of the drone dynamics, |
| 4 | +observations, and cost functions using CasADi. It includes: |
| 5 | +
|
| 6 | +* :class:`~.SymbolicModel`: A class that encapsulates the symbolic representation of the drone |
| 7 | + model, including dynamics, observations, and cost functions. |
| 8 | +* :func:`symbolic <lsy_drone_racing.sim.symbolic.symbolic>`: A function that creates a |
| 9 | + :class:`~.SymbolicModel` instance for a given drone configuration. |
| 10 | +* Helper functions for creating rotation matrices and other mathematical operations. |
| 11 | +
|
| 12 | +The symbolic models created by this module can be used for various control and estimation tasks, |
| 13 | +such as model predictive control (MPC) or state estimation. They provide a convenient way to work |
| 14 | +with the nonlinear dynamics of the drone in a symbolic framework, allowing for automatic |
| 15 | +differentiation and optimization. |
| 16 | +""" |
| 17 | + |
| 18 | + |
| 19 | +import casadi as cs |
| 20 | +from casadi import MX |
| 21 | + |
| 22 | +from crazyflow.constants import GRAVITY, ARM_LEN |
| 23 | +from crazyflow.sim.core import Sim |
| 24 | +from crazyflow.control.controller import KF, KM |
| 25 | + |
| 26 | + |
| 27 | +class SymbolicModel: |
| 28 | + """Implement the drone dynamics model with symbolic variables. |
| 29 | +
|
| 30 | + x_dot = f(x,u), y = g(x,u), with other pre-defined, symbolic functions (e.g. cost, constraints), |
| 31 | + serve as priors for the controllers. |
| 32 | +
|
| 33 | + Notes: |
| 34 | + * naming convention on symbolic variable and functions. |
| 35 | + * for single-letter symbol, use {}_sym, otherwise use underscore for delimiter. |
| 36 | + * for symbolic functions to be exposed, use {}_func. |
| 37 | + """ |
| 38 | + |
| 39 | + def __init__( |
| 40 | + self, dynamics: dict[str, MX], cost: dict[str, MX], dt: float = 1e-3, solver: str = "cvodes" |
| 41 | + ): |
| 42 | + """Initialize the symbolic model. |
| 43 | +
|
| 44 | + Args: |
| 45 | + dynamics: A dictionary containing the dynamics, observation functions and variables. |
| 46 | + cost: A dictionary containing the cost function and its variables. |
| 47 | + dt: The sampling time. |
| 48 | + solver: The integration algorithm. |
| 49 | + """ |
| 50 | + self.x_sym = dynamics["vars"]["X"] |
| 51 | + self.u_sym = dynamics["vars"]["U"] |
| 52 | + self.x_dot = dynamics["dyn_eqn"] |
| 53 | + self.y_sym = self.x_sym if dynamics["obs_eqn"] is None else dynamics["obs_eqn"] |
| 54 | + self.dt = dt # Sampling time. |
| 55 | + self.solver = solver # Integration algorithm |
| 56 | + # Variable dimensions. |
| 57 | + self.nx = self.x_sym.shape[0] |
| 58 | + self.nu = self.u_sym.shape[0] |
| 59 | + self.ny = self.y_sym.shape[0] |
| 60 | + # Setup cost function. |
| 61 | + self.cost_func = cost["cost_func"] |
| 62 | + self.Q = cost["vars"]["Q"] |
| 63 | + self.R = cost["vars"]["R"] |
| 64 | + self.Xr = cost["vars"]["Xr"] |
| 65 | + self.Ur = cost["vars"]["Ur"] |
| 66 | + self.setup_model() |
| 67 | + self.setup_linearization() # Setup Jacobian and Hessian of the dynamics and cost functions |
| 68 | + |
| 69 | + def setup_model(self): |
| 70 | + """Expose functions to evaluate the model.""" |
| 71 | + # Continuous time dynamics. |
| 72 | + self.fc_func = cs.Function("fc", [self.x_sym, self.u_sym], [self.x_dot], ["x", "u"], ["f"]) |
| 73 | + # Discrete time dynamics. |
| 74 | + self.fd_func = cs.integrator( |
| 75 | + "fd", self.solver, {"x": self.x_sym, "p": self.u_sym, "ode": self.x_dot}, 0, self.dt |
| 76 | + ) |
| 77 | + # Observation model. |
| 78 | + self.g_func = cs.Function("g", [self.x_sym, self.u_sym], [self.y_sym], ["x", "u"], ["g"]) |
| 79 | + |
| 80 | + def setup_linearization(self): |
| 81 | + """Expose functions for the linearized model.""" |
| 82 | + # Jacobians w.r.t state & input. |
| 83 | + self.dfdx = cs.jacobian(self.x_dot, self.x_sym) |
| 84 | + self.dfdu = cs.jacobian(self.x_dot, self.u_sym) |
| 85 | + self.df_func = cs.Function( |
| 86 | + "df", [self.x_sym, self.u_sym], [self.dfdx, self.dfdu], ["x", "u"], ["dfdx", "dfdu"] |
| 87 | + ) |
| 88 | + self.dgdx = cs.jacobian(self.y_sym, self.x_sym) |
| 89 | + self.dgdu = cs.jacobian(self.y_sym, self.u_sym) |
| 90 | + self.dg_func = cs.Function( |
| 91 | + "dg", [self.x_sym, self.u_sym], [self.dgdx, self.dgdu], ["x", "u"], ["dgdx", "dgdu"] |
| 92 | + ) |
| 93 | + # Evaluation point for linearization. |
| 94 | + self.x_eval = cs.MX.sym("x_eval", self.nx, 1) |
| 95 | + self.u_eval = cs.MX.sym("u_eval", self.nu, 1) |
| 96 | + # Linearized dynamics model. |
| 97 | + self.x_dot_linear = ( |
| 98 | + self.x_dot |
| 99 | + + self.dfdx @ (self.x_eval - self.x_sym) |
| 100 | + + self.dfdu @ (self.u_eval - self.u_sym) |
| 101 | + ) |
| 102 | + self.fc_linear_func = cs.Function( |
| 103 | + "fc", |
| 104 | + [self.x_eval, self.u_eval, self.x_sym, self.u_sym], |
| 105 | + [self.x_dot_linear], |
| 106 | + ["x_eval", "u_eval", "x", "u"], |
| 107 | + ["f_linear"], |
| 108 | + ) |
| 109 | + self.fd_linear_func = cs.integrator( |
| 110 | + "fd_linear", |
| 111 | + self.solver, |
| 112 | + { |
| 113 | + "x": self.x_eval, |
| 114 | + "p": cs.vertcat(self.u_eval, self.x_sym, self.u_sym), |
| 115 | + "ode": self.x_dot_linear, |
| 116 | + }, |
| 117 | + 0, |
| 118 | + self.dt, |
| 119 | + ) |
| 120 | + # Linearized observation model. |
| 121 | + self.y_linear = ( |
| 122 | + self.y_sym |
| 123 | + + self.dgdx @ (self.x_eval - self.x_sym) |
| 124 | + + self.dgdu @ (self.u_eval - self.u_sym) |
| 125 | + ) |
| 126 | + self.g_linear_func = cs.Function( |
| 127 | + "g_linear", |
| 128 | + [self.x_eval, self.u_eval, self.x_sym, self.u_sym], |
| 129 | + [self.y_linear], |
| 130 | + ["x_eval", "u_eval", "x", "u"], |
| 131 | + ["g_linear"], |
| 132 | + ) |
| 133 | + # Jacobian and Hessian of cost function. |
| 134 | + self.l_x = cs.jacobian(self.cost_func, self.x_sym) |
| 135 | + self.l_xx = cs.jacobian(self.l_x, self.x_sym) |
| 136 | + self.l_u = cs.jacobian(self.cost_func, self.u_sym) |
| 137 | + self.l_uu = cs.jacobian(self.l_u, self.u_sym) |
| 138 | + self.l_xu = cs.jacobian(self.l_x, self.u_sym) |
| 139 | + l_inputs = [self.x_sym, self.u_sym, self.Xr, self.Ur, self.Q, self.R] |
| 140 | + l_inputs_str = ["x", "u", "Xr", "Ur", "Q", "R"] |
| 141 | + l_outputs = [self.cost_func, self.l_x, self.l_xx, self.l_u, self.l_uu, self.l_xu] |
| 142 | + l_outputs_str = ["l", "l_x", "l_xx", "l_u", "l_uu", "l_xu"] |
| 143 | + self.loss = cs.Function("loss", l_inputs, l_outputs, l_inputs_str, l_outputs_str) |
| 144 | + |
| 145 | + |
| 146 | +def symbolic(sim: Sim, dt: float) -> SymbolicModel: |
| 147 | + """Create symbolic (CasADi) models for dynamics, observation, and cost of a quadcopter. |
| 148 | +
|
| 149 | + Returns: |
| 150 | + The CasADi symbolic model of the environment. |
| 151 | + """ |
| 152 | + m, g = sim.params.mass[0,0,1], GRAVITY |
| 153 | + # Define states. |
| 154 | + z = cs.MX.sym("z") |
| 155 | + z_dot = cs.MX.sym("z_dot") |
| 156 | + |
| 157 | + # Set up the dynamics model for a 3D quadrotor. |
| 158 | + nx, nu = 12, 4 |
| 159 | + Ixx, Iyy, Izz = sim.params.J[0,0].diagonal() |
| 160 | + J = cs.blockcat([[Ixx, 0.0, 0.0], [0.0, Iyy, 0.0], [0.0, 0.0, Izz]]) |
| 161 | + Jinv = cs.blockcat([[1.0 / Ixx, 0.0, 0.0], [0.0, 1.0 / Iyy, 0.0], [0.0, 0.0, 1.0 / Izz]]) |
| 162 | + gamma = KM / KF |
| 163 | + x = cs.MX.sym("x") |
| 164 | + y = cs.MX.sym("y") |
| 165 | + phi = cs.MX.sym("phi") # Roll |
| 166 | + theta = cs.MX.sym("theta") # Pitch |
| 167 | + psi = cs.MX.sym("psi") # Yaw |
| 168 | + x_dot = cs.MX.sym("x_dot") |
| 169 | + y_dot = cs.MX.sym("y_dot") |
| 170 | + p = cs.MX.sym("p") # Body frame roll rate |
| 171 | + q = cs.MX.sym("q") # body frame pith rate |
| 172 | + r = cs.MX.sym("r") # body frame yaw rate |
| 173 | + # Rotation matrix transforming a vector in the body frame to the world frame. PyBullet Euler |
| 174 | + # angles use the SDFormat for rotation matrices. |
| 175 | + Rob = csRotXYZ(phi, theta, psi) |
| 176 | + # Define state variables. |
| 177 | + X = cs.vertcat(x, x_dot, y, y_dot, z, z_dot, phi, theta, psi, p, q, r) |
| 178 | + # Define inputs. |
| 179 | + f1 = cs.MX.sym("f1") |
| 180 | + f2 = cs.MX.sym("f2") |
| 181 | + f3 = cs.MX.sym("f3") |
| 182 | + f4 = cs.MX.sym("f4") |
| 183 | + U = cs.vertcat(f1, f2, f3, f4) |
| 184 | + |
| 185 | + # From Ch. 2 of Luis, Carlos, and Jérôme Le Ny. "Design of a trajectory tracking |
| 186 | + # controller for a nanoquadcopter." arXiv preprint arXiv:1608.05786 (2016). |
| 187 | + |
| 188 | + # Defining the dynamics function. |
| 189 | + # We are using the velocity of the base wrt to the world frame expressed in the world frame. |
| 190 | + # Note that the reference expresses this in the body frame. |
| 191 | + oVdot_cg_o = Rob @ cs.vertcat(0, 0, f1 + f2 + f3 + f4) / m - cs.vertcat(0, 0, g) |
| 192 | + pos_ddot = oVdot_cg_o |
| 193 | + pos_dot = cs.vertcat(x_dot, y_dot, z_dot) |
| 194 | + Mb = cs.vertcat( |
| 195 | + ARM_LEN / cs.sqrt(2.0) * (f1 + f2 - f3 - f4), |
| 196 | + ARM_LEN / cs.sqrt(2.0) * (-f1 + f2 + f3 - f4), |
| 197 | + gamma * (f1 - f2 + f3 - f4), |
| 198 | + ) |
| 199 | + rate_dot = Jinv @ (Mb - (cs.skew(cs.vertcat(p, q, r)) @ J @ cs.vertcat(p, q, r))) |
| 200 | + ang_dot = cs.blockcat( |
| 201 | + [ |
| 202 | + [1, cs.sin(phi) * cs.tan(theta), cs.cos(phi) * cs.tan(theta)], |
| 203 | + [0, cs.cos(phi), -cs.sin(phi)], |
| 204 | + [0, cs.sin(phi) / cs.cos(theta), cs.cos(phi) / cs.cos(theta)], |
| 205 | + ] |
| 206 | + ) @ cs.vertcat(p, q, r) |
| 207 | + X_dot = cs.vertcat( |
| 208 | + pos_dot[0], pos_ddot[0], pos_dot[1], pos_ddot[1], pos_dot[2], pos_ddot[2], ang_dot, rate_dot |
| 209 | + ) |
| 210 | + |
| 211 | + Y = cs.vertcat(x, x_dot, y, y_dot, z, z_dot, phi, theta, psi, p, q, r) |
| 212 | + |
| 213 | + # Define cost (quadratic form). |
| 214 | + Q = cs.MX.sym("Q", nx, nx) |
| 215 | + R = cs.MX.sym("R", nu, nu) |
| 216 | + Xr = cs.MX.sym("Xr", nx, 1) |
| 217 | + Ur = cs.MX.sym("Ur", nu, 1) |
| 218 | + cost_func = 0.5 * (X - Xr).T @ Q @ (X - Xr) + 0.5 * (U - Ur).T @ R @ (U - Ur) |
| 219 | + # Define dynamics and cost dictionaries. |
| 220 | + dynamics = {"dyn_eqn": X_dot, "obs_eqn": Y, "vars": {"X": X, "U": U}} |
| 221 | + cost = {"cost_func": cost_func, "vars": {"X": X, "U": U, "Xr": Xr, "Ur": Ur, "Q": Q, "R": R}} |
| 222 | + return SymbolicModel(dynamics=dynamics, cost=cost, dt=dt) |
| 223 | + |
| 224 | + |
| 225 | +def csRotXYZ(phi: float, theta: float, psi: float) -> cs.MX: |
| 226 | + """Create a rotation matrix from euler angles. |
| 227 | +
|
| 228 | + This represents the extrinsic X-Y-Z (or quivalently the intrinsic Z-Y-X (3-2-1)) euler angle |
| 229 | + rotation. |
| 230 | +
|
| 231 | + Args: |
| 232 | + phi: roll (or rotation about X). |
| 233 | + theta: pitch (or rotation about Y). |
| 234 | + psi: yaw (or rotation about Z). |
| 235 | +
|
| 236 | + Returns: |
| 237 | + R: casadi Rotation matrix |
| 238 | + """ |
| 239 | + rx = cs.blockcat([[1, 0, 0], [0, cs.cos(phi), -cs.sin(phi)], [0, cs.sin(phi), cs.cos(phi)]]) |
| 240 | + ry = cs.blockcat( |
| 241 | + [[cs.cos(theta), 0, cs.sin(theta)], [0, 1, 0], [-cs.sin(theta), 0, cs.cos(theta)]] |
| 242 | + ) |
| 243 | + rz = cs.blockcat([[cs.cos(psi), -cs.sin(psi), 0], [cs.sin(psi), cs.cos(psi), 0], [0, 0, 1]]) |
| 244 | + return rz @ ry @ rx |
| 245 | + |
| 246 | +# UNCOMMENT BELOW TO VERIFY INITIALIZATION OF SYMBOLIC MODEL |
| 247 | + |
| 248 | +# from ml_collections import config_dict |
| 249 | +# device = "cpu" |
| 250 | +# sim_config = config_dict.ConfigDict() |
| 251 | +# sim_config.n_worlds = 10 |
| 252 | +# sim_config.n_drones = 1 |
| 253 | +# sim_config.physics = "sys_id" |
| 254 | +# sim_config.control = "attitude" |
| 255 | +# sim_config.controller = "emulatefirmware" |
| 256 | +# sim_config.device = device |
| 257 | +# sim = Sim(**sim_config) |
| 258 | + |
| 259 | +# symbolic(sim, sim.dt) |
| 260 | + |
0 commit comments