1
$\begingroup$

I am trying to implement an LQR controller for a quadrotor to perform reference tracking. My goal is to deploy this controller in a MuJoCo simulation using ROS2. The actuators of the drone are abstracted to resemble thrust, and x, y, and z moments. I use the linear model obtained from here (pages 20-21). From what I understand, this should be enough to get a good enough controller for non-extreme reference/trajectory tracking.

I computed the LQR gain $K$ using MATLAB. First, the linear dynamics are converted to discrete-time using c2d(), whence a discrete-time LQR controller is implemented using dlqr(). The system is discretized using the same sample time as the controller in ROS2, that is, $Ts = 0.002$. The closed-loop eigenvalues are checked, and indeed the closed-loop is stable with all eigenvalues $\lambda$ satisfying $\lambda < |1| $. The control law is $u = -K(x - x_{ref})$. Below is the plot for 3D position $(1, 1, 1)$. Note that $(x,y,z,u,v,w)$ are the position (world frame) and corresponding linear velocities (body frame) and $(phi, theta, psi, p, q, r)$ are the angles (world frame) and corresponding angular velocities (body frame).

enter image description here

My problem is the following. When I implement the LQR controller in my ROS2 node to control the drone in the simulation, with the gain obtained from MATLAB, only the thrust control input does what it's supposed to. However, the roll, pitch and yaw commands seem to be faulty since it gradually starts rolling and pitch until it crashes (despite the reference being $(0, 0, 0.5)$). When setting the yaw reference to $1$, it just starts spinning. My current suspicion is that the angles obtained from the resulting control input must be manipulated in some way before being published to the actuators of the drone.

Why does the LQR gain stabilize the linear system in MATLAB but not the drone in the simulation? Could there be a significant discrepancy in the dynamics? According to this project, I believe we have the same framework, so I guess it is doable. I'm really at a loss as to what the problem is.

#!/usr/bin/env python3 import rclpy from rclpy.node import Node import numpy as np from example_interfaces.msg import Float64, Float64MultiArray import math class LQRController(Node): def __init__(self): super().__init__('lqr_controller') # ---------- TARGETS ---------- self.x_ref = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5]) # ---------- STATES ---------- self.position = np.zeros(3) self.quaternion = np.array([1.0, 0.0, 0.0, 0.0]) self.x = np.zeros(12) self.last_position = np.zeros(3) self.last_angles = np.zeros(3) # ---------- GAIN ---------- self.K = np.array([ [-0.0000, -0.0000, -0.0000, -0.0000, -0.0000, -0.0000, 0.0000, -0.0000, -13.3334, 0.0000, -0.0000, -41.8947], [ 0.1223, 0.0000, 0.0000, 0.0121, 0.0000, -0.0000, -0.0000, 0.0572, 0.0000, 0.0000, 0.0750, -0.0000], [ 0.0000, 0.1223, -0.0000, -0.0000, 0.0121, -0.0000, -0.0572, 0.0000, 0.0000, -0.0750, 0.0000, 0.0000], [-0.0000, -0.0000, 0.0510, -0.0000, -0.0000, 0.0162, 0.0000, -0.0000, 0.0000, 0.0000, -0.0000, 0.0000] ]) # ---------- TIMING ---------- self.dt = 0.002 # 500 Hz # ---------- ROS ---------- # Publishers self.thrust_pub = self.create_publisher(Float64, '/cf2/actuators/body_thrust/command', 10) self.roll_pub = self.create_publisher(Float64, '/cf2/actuators/x_moment/command', 10) self.pitch_pub = self.create_publisher(Float64, '/cf2/actuators/y_moment/command', 10) self.yaw_pub = self.create_publisher(Float64, '/cf2/actuators/z_moment/command', 10) # Subscribers self.pos_sub = self.create_subscription(Float64MultiArray, '/cf2/pos', self.position_callback, 10) self.quat_sub = self.create_subscription(Float64MultiArray, '/cf2/quat', self.quaternion_callback, 10) # Loop timers self.loop_timer = self.create_timer(self.dt, self.control_loop) # Logging self.log_cycle = 0 self.get_logger().info("LQR initialized.") # ---------- CALLBACKS ---------- def position_callback(self, msg): self.position = np.array(msg.data) def quaternion_callback(self, msg): self.quaternion = list(msg.data) # ---------- Control LOOP ---------- def control_loop(self): # Extract Euler angles from quaternion qw, qx, qy, qz = self.quaternion roll = math.atan2(2*(qw*qx + qy*qz), 1 - 2*(qx*qx + qy*qy)) pitch = math.asin(max(min(2*(qw*qy - qz*qx), 1.0), -1.0)) yaw = math.atan2(2*(qw*qz + qx*qy), 1 - 2*(qy*qy + qz*qz)) cphi = np.cos(roll) sphi = np.sin(roll) ctheta = np.cos(pitch) stheta = np.sin(pitch) cpsi = np.cos(yaw) spsi = np.sin(yaw) ttheta = np.tan(pitch) R = np.array([ [ctheta * cpsi, sphi * stheta * cpsi - cphi * spsi, cphi * stheta * cpsi + sphi * spsi], [ctheta * spsi, sphi * stheta * spsi + cphi * cpsi, cphi * stheta * spsi - sphi * cpsi], [-stheta, sphi * ctheta, cphi * ctheta] ]) T = np.array([ [1, sphi * ttheta, cphi * ttheta], [0, cphi, -sphi], [0, sphi / ctheta, cphi / ctheta] ]) angles = np.array([roll, pitch, yaw]) avel = (angles - self.last_angles) / self.dt self.last_angles = angles avel = T.transpose() @ avel pos = self.position vel = (pos - self.last_position) / self.dt self.last_position = self.position vel = R.transpose() @ vel self.x = np.array([roll, pitch, yaw, avel[0], avel[1], avel[2], vel[0], vel[1], vel[2], pos[0], pos[1], pos[2]]) error = self.x - self.x_ref u = -self.K.dot(error) # Publish actuator commands self.thrust_pub.publish(Float64(data=-u[0])) self.roll_pub.publish(Float64(data=u[1])) self.pitch_pub.publish(Float64(data=u[2])) self.yaw_pub.publish(Float64(data=u[3])) # Log at 10Hz self.log_cycle += 1 if self.log_cycle % 50 == 0: self.get_logger().info( f"Pos [{float(pos[0]):.3f}, {float(pos[1]):.3f}, {float(pos[2]):.3f}] | " f"Thrust {float(u[0]):.3f}" ) def main(args=None): rclpy.init(args=args) node = LQRController() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main() 
$\endgroup$
15
  • $\begingroup$ What do you mean when you say, "the closed-loop is stable with all eigenvalues $\lambda$ satisfying $\lambda <|1|$"? I can't tell if you're referring to a Z-transform, where the values need to be inside the unit circle for stability (which is where <|1| would mean stable), or if you actually mean the eigenvalues are <1. The (real part of the) eigenvalues need to be negative, <0, to be stable. $\endgroup$ Commented Nov 13 at 16:29
  • $\begingroup$ Other things that look suspicious: Where did you get the math for calculating pitch? Red flag there that you're using asin instead of atan2 like the roll/yaw calculations, and this min/max game you're playing in the operand. What is T, where did that math come from, and why are you using it with avel? You said angles are in the world frame and the angular velocities are in the body frame, but you calculate the angular velocities as the difference in angles divided by dt. If T is supposed to convert frames then what does R do? $\endgroup$ Commented Nov 13 at 16:38
  • 1
    $\begingroup$ Finally, you really need plots to help troubleshoot control issues. The Matlab plots are a little confusing because it looks like you're commanding all three positions to +1, but also looks like you're starting with velocities active. I'd personally prefer to see it start at steady-state with a step command at t=1 or something similar. You also don't have a step response for the yaw angle (that I can tell). And, these are all the plots in Matlab. You've got logging in the Python code, but no plot. Expand the logging to include reference and feedback values for all axes. $\endgroup$ Commented Nov 13 at 16:59
  • $\begingroup$ Given that the system is now in discrete-time, inner-stability of the closed loop is defined like that. The roll, pitch, yaw calculations are standard aerospace convention. R and T are obtained from where I obtained the dynamics. R is for the linear velocities, and T for the angular velocities. The velocities start at 0, but make a strong correction early on, so it seems like it may be starting from non-zero values. $\endgroup$ Commented Nov 13 at 17:25
  • 1
    $\begingroup$ I see you've put a bounty on this, and I want you to get help, but you need to provide more information. You said it works in Matlab but not in ROS, but you only gave us plots of the behavior in Matlab. It could be one of your nodes is or is isn't using sim time, topic naming issues (like you aren't getting the feedback you think you're getting), etc. Plot juggler or foxglove can both be used to make nice plots of reference and feedback values, which would be the first thing to look at when troubleshooting a control system. $\endgroup$ Commented Nov 15 at 16:30

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.