Skip to content

Commit e4f4a68

Browse files
committed
add symbolic model
1 parent 850f494 commit e4f4a68

File tree

2 files changed

+261
-0
lines changed

2 files changed

+261
-0
lines changed

crazyflow/sim/symbolic.py

+260
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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+

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies = [
2626
"einops",
2727
"flax",
2828
"ml_collections",
29+
"casadi"
2930
]
3031

3132
[project.optional-dependencies]

0 commit comments

Comments
 (0)