Skip to content

Commit 293f56c

Browse files
committed
added hover script to test force estimator
1 parent 9fab3f2 commit 293f56c

File tree

4 files changed

+212
-28
lines changed

4 files changed

+212
-28
lines changed

config/level3.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ file = "trajectory_controller.py" # Put your controller file name here. Specifyi
99
[deploy]
1010
### Settings only relevant for deployment
1111
# Whether to check if gate and obstacle positions observed by vicon are within the limits defined down below.
12-
check_race_track = true
12+
check_race_track = false
1313
# Whether to check if the drone start position is within the limits specified down below.
1414
check_drone_start_pos = true
1515
# Lets you practice your controller without putting up gates & obstacles, assumes nominal positions given below.
16-
practice_without_track_objects = false
16+
practice_without_track_objects = true
1717

1818
[sim]
1919
# Physics options:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""This module implements a ThrustController for quadrotor control.
2+
3+
It utilizes the collective thrust interface for drone control to compute control commands based on
4+
current state observations and desired waypoints. The attitude control is handled by computing a
5+
PID control law for position tracking, incorporating gravity compensation in thrust calculations.
6+
7+
The waypoints are generated using cubic spline interpolation from a set of predefined waypoints.
8+
Note that the trajectory uses pre-defined waypoints instead of dynamically generating a good path.
9+
"""
10+
11+
from __future__ import annotations # Python 3.10 type hints
12+
13+
import math
14+
from typing import TYPE_CHECKING
15+
16+
import numpy as np
17+
import pybullet as p
18+
from scipy.interpolate import CubicSpline
19+
from scipy.spatial.transform import Rotation as R
20+
21+
from lsy_drone_racing.control import BaseController
22+
23+
if TYPE_CHECKING:
24+
from numpy.typing import NDArray
25+
26+
27+
class ThrustController(BaseController):
28+
"""Example of a controller using the collective thrust and attitude interface.
29+
30+
Modified from https://github.com/utiasDSL/crazyswarm-import/blob/ad2f7ea987f458a504248a1754b124ba39fc2f21/ros_ws/src/crazyswarm/scripts/position_ctl_m.py
31+
"""
32+
33+
def __init__(self, initial_obs: dict[str, NDArray[np.floating]], initial_info: dict):
34+
"""Initialization of the controller.
35+
36+
Args:
37+
initial_obs: The initial observation of the environment's state. See the environment's
38+
observation space for details.
39+
initial_info: Additional environment information from the reset.
40+
"""
41+
super().__init__(initial_obs, initial_info)
42+
self.low_level_ctrl_freq = initial_info["low_level_ctrl_freq"]
43+
self.drone_mass = initial_info["drone_mass"]
44+
self.kp = np.array([0.4, 0.4, 1.25])
45+
self.ki = np.array([0.05, 0.05, 0.05])
46+
self.kd = np.array([0.2, 0.2, 0.4])
47+
self.ki_range = np.array([2.0, 2.0, 0.4])
48+
self.i_error = np.zeros(3)
49+
self.g = 9.81
50+
self._tick = 0
51+
52+
# Same waypoints as in the trajectory controller. Determined by trial and error.
53+
waypoints = np.array(
54+
[
55+
[1.0, 1.0, 0.05],
56+
[1.0, 1.0, 1.0],
57+
[1.0, 1.0, 1.0],
58+
[1.0, 1.0, 1.0],
59+
[1.0, 1.0, 1.0],
60+
]
61+
)
62+
# Scale trajectory between 0 and 1
63+
ts = np.linspace(0, 1, np.shape(waypoints)[0])
64+
cs_x = CubicSpline(ts, waypoints[:, 0])
65+
cs_y = CubicSpline(ts, waypoints[:, 1])
66+
cs_z = CubicSpline(ts, waypoints[:, 2])
67+
68+
des_completion_time = 30
69+
ts = np.linspace(0, 1, int(initial_info["env_freq"] * des_completion_time))
70+
71+
self.x_des = cs_x(ts)
72+
self.y_des = cs_y(ts)
73+
self.z_des = cs_z(ts)
74+
75+
try:
76+
# Draw interpolated Trajectory. Limit segments to avoid excessive drawings
77+
stride = max(1, len(self.x_des) // 100)
78+
trajectory = np.vstack([self.x_des, self.y_des, self.z_des])[..., ::stride].T
79+
for i in range(len(trajectory) - 1):
80+
p.addUserDebugLine(
81+
trajectory[i],
82+
trajectory[i + 1],
83+
lineColorRGB=[1, 0, 0],
84+
lineWidth=2,
85+
lifeTime=0,
86+
physicsClientId=0,
87+
)
88+
except p.error:
89+
... # Ignore pybullet errors if not running in the pybullet GUI
90+
91+
def compute_control(
92+
self, obs: dict[str, NDArray[np.floating]], info: dict | None = None
93+
) -> NDArray[np.floating]:
94+
"""Compute the next desired collective thrust and roll/pitch/yaw of the drone.
95+
96+
Args:
97+
obs: The current observation of the environment. See the environment's observation space
98+
for details.
99+
info: Optional additional information as a dictionary.
100+
101+
Returns:
102+
The collective thrust and orientation [t_des, r_des, p_des, y_des] as a numpy array.
103+
"""
104+
i = min(self._tick, len(self.x_des) - 1)
105+
des_pos = np.array([self.x_des[i], self.y_des[i], self.z_des[i]])
106+
des_vel = np.zeros(3)
107+
des_yaw = 0.0
108+
109+
# Calculate the deviations from the desired trajectory
110+
pos_error = des_pos - obs["pos"]
111+
vel_error = des_vel - obs["vel"]
112+
113+
# Update integral error
114+
self.i_error += pos_error * (1 / self.low_level_ctrl_freq)
115+
self.i_error = np.clip(self.i_error, -self.ki_range, self.ki_range)
116+
117+
# Compute target thrust
118+
target_thrust = np.zeros(3)
119+
target_thrust += self.kp * pos_error
120+
target_thrust += self.ki * self.i_error
121+
target_thrust += self.kd * vel_error
122+
# target_thrust += params.quad.m * desired_acc
123+
target_thrust[2] += self.drone_mass * self.g
124+
125+
# Update z_axis to the current orientation of the drone
126+
z_axis = R.from_euler("xyz", obs["rpy"]).as_matrix()[:, 2]
127+
128+
# update current thrust
129+
thrust_desired = target_thrust.dot(z_axis)
130+
thrust_desired = max(thrust_desired, 0.3 * self.drone_mass * self.g)
131+
thrust_desired = min(thrust_desired, 1.8 * self.drone_mass * self.g)
132+
133+
# update z_axis_desired
134+
z_axis_desired = target_thrust / np.linalg.norm(target_thrust)
135+
x_c_des = np.array([math.cos(des_yaw), math.sin(des_yaw), 0.0])
136+
y_axis_desired = np.cross(z_axis_desired, x_c_des)
137+
y_axis_desired /= np.linalg.norm(y_axis_desired)
138+
x_axis_desired = np.cross(y_axis_desired, z_axis_desired)
139+
140+
R_desired = np.vstack([x_axis_desired, y_axis_desired, z_axis_desired]).T
141+
euler_desired = R.from_matrix(R_desired).as_euler("xyz", degrees=False)
142+
thrust_desired, euler_desired
143+
return np.concatenate([[thrust_desired], euler_desired])
144+
145+
def step_callback(
146+
self,
147+
action: NDArray[np.floating],
148+
obs: dict[str, NDArray[np.floating]],
149+
reward: float,
150+
terminated: bool,
151+
truncated: bool,
152+
info: dict,
153+
):
154+
"""Increment the tick counter."""
155+
self._tick += 1
156+
157+
def episode_callback(self):
158+
"""Reset the integral error."""
159+
self.i_error[:] = 0
160+
self._tick = 0

lsy_drone_racing/utils/disturbance_observer.py

+44-25
Original file line numberDiff line numberDiff line change
@@ -201,33 +201,52 @@ def __init__(self, dt: np.floating, model_name: str = "analytical_mel_att", init
201201
self._state = self._UKF.x
202202

203203
# Set process noise covariance (tunable). Uncertainty in the dynamics. High Q -> less trust in model
204-
# Q_x = Q_discrete_white_noise(dim=2, dt=dt, var=1e-9, block_size=6, order_by_dim=False)
205-
# Q_f = np.eye(3)*1e-6 # Q_discrete_white_noise(dim=3, dt=dt, var=1e-6, block_size=1, order_by_dim=False)
206-
# Q_t = np.eye(3)*1e-6 # Q_discrete_white_noise(dim=3, dt=dt, var=1e-6, block_size=1, order_by_dim=False)
207-
# self._UKF.Q = block_diag(Q_x, Q_f, Q_t) # np.eye(18)*1e-6 #
208-
# self._UKF.Q[12:15, 12:15] = self._UKF.Q[12:15, 12:15] * 1e1 # Force
209-
# self._UKF.Q[15:18, 15:18] = self._UKF.Q[15:18, 15:18] * 1e1 # Torque
210-
211-
self._varQ = 1e-2
212-
self._varR = 1e-3
213-
214-
# self._UKF.Q = Q_discrete_white_noise(dim=3, dt=self._dt, var=self._varQ, block_size=6, order_by_dim=False) # TODO manually setup matrix
215-
# self._UKF.Q[12:15, 12:15] = self._UKF.Q[12:15, 12:15] * 5e0 # Force
216-
# self._UKF.Q[15:18, 15:18] = self._UKF.Q[15:18, 15:18] * 5e0 # Torque
217-
# self._UKF.Q = np.eye(18)*1e-9
218-
# This way, pos, vel, and force (or rpy, angular vel, and torque) influence each other
219-
# print(self._UKF.Q.tolist())
220-
221-
Q_p = np.eye(3)*self._varQ*1e-0
222-
Q_a = np.eye(3)*self._varQ*1e-0
223-
Q_v = np.eye(3)*self._varQ*1e-0
224-
Q_w = np.eye(3)*self._varQ*1e-0
225-
Q_f = np.eye(3)*self._varQ*1e-0
226-
Q_t = np.eye(3)*self._varQ*1e-0
227-
self._UKF.Q = block_diag(Q_p, Q_a, Q_v, Q_w, Q_f, Q_t)
204+
self._UKF.Q = np.eye(dim_x)
205+
# self._varQ = 1e-10
206+
# Q_x = Q_discrete_white_noise(dim=2, dt=dt, var=self._varQ, block_size=6, order_by_dim=False)
207+
# Q_f = np.eye(3)*1e0 # Q_discrete_white_noise(dim=3, dt=dt, var=1e-6, block_size=1, order_by_dim=False)
208+
# Q_t = np.eye(3)*1e0 # Q_discrete_white_noise(dim=3, dt=dt, var=1e-6, block_size=1, order_by_dim=False)
209+
# self._KF.Q = block_diag(Q_x, Q_f, Q_t) # np.eye(18)*1e-6 #
210+
# self._KF.Q[12:15, 12:15] = self._KF.Q[12:15, 12:15] * 1e1 # Force
211+
# self._KF.Q[15:18, 15:18] = self._KF.Q[15:18, 15:18] * 1e1 # Torque
212+
213+
# self._varQ = 1e-1
214+
# self._KF.Q = Q_discrete_white_noise(dim=3, dt=self._dt, var=self._varQ, block_size=dim_x//3, order_by_dim=False) # TODO manually setup matrix
215+
216+
# See sketch for more info on how the matrix is set up
217+
Q_xyz = Q_discrete_white_noise(dim=2, dt=self._dt, var=1e-1, block_size=3, order_by_dim=False) # pos & vel
218+
# plt.matshow(Q_xyz)
219+
# plt.show()
220+
self._UKF.Q[0:3,0:3] = Q_xyz[0:3,0:3] # pos
221+
self._UKF.Q[6:9,6:9] = Q_xyz[3:6,3:6] # vel
222+
self._UKF.Q[0:3,6:9] = Q_xyz[0:3,3:6] # pos <-> vel
223+
self._UKF.Q[6:9,0:3] = Q_xyz[3:6,0:3] # pos <-> vel
224+
225+
Q_rpy = Q_discrete_white_noise(dim=2, dt=self._dt, var=1e-0, block_size=3, order_by_dim=False) # rpy & rpy rates
226+
# plt.matshow(Q_rpy)
227+
# plt.show()
228+
self._UKF.Q[3:6,3:6] = Q_rpy[0:3,0:3] # rpy
229+
self._UKF.Q[9:12,9:12] = Q_rpy[3:6,3:6] # rpy rates
230+
self._UKF.Q[3:6,9:12] = Q_rpy[0:3,3:6] # rpy <-> rpy rates
231+
self._UKF.Q[9:12,3:6] = Q_rpy[3:6,0:3] # rpy <-> rpy rates
232+
233+
234+
235+
# self._KF.Q[0:3] = self._KF.Q[0:3] * 1e-1 # pos
236+
# self._KF.Q[3:6] = self._KF.Q[3:6] * 1e-1 # rpy
237+
# self._KF.Q[6:9] = self._KF.Q[6:9] * 1e-1 # vel
238+
# self._KF.Q[9:12] = self._KF.Q[9:12] * 1e-1 # rpy rate
239+
240+
if dim_x > 12:
241+
self._UKF.Q[12:15, 12:15] *= 1e-5 # Force
242+
self._UKF.Q[15:18, 15:18] *= 1e-9 # Torque
228243

229244
# Set measurement noise covariance (tunable). Uncertaints in the measurements. High R -> less trust in measurements
230-
self._UKF.R = np.eye(self._obs_dim) * self._varR
245+
self._UKF.R = np.eye(self._obs_dim)
246+
# very low noise on the position ("mm precision" => even less noise)
247+
self._UKF.R[:3, :3] = self._UKF.R[:3, :3] * 1e-8
248+
# "high" measurements noise on the angles, estimate: 0.01 constains all values => std=3e-3
249+
self._UKF.R[3:, 3:] = self._UKF.R[3:, 3:] * 3e-3 # 1e-5 works quite well, but bias!
231250

232251
# Initialize state and covariance
233252
if initial_obs is not None:

lsy_drone_racing/vicon.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import numpy as np
1919
import rospy
2020
import yaml
21-
from crazyswarm.msg import StateVector
21+
from vicon_bridge.msg import StateVector, Command
2222
from rosgraph import Master
2323
from scipy.spatial.transform import Rotation as R
2424
from tf2_msgs.msg import TFMessage
@@ -71,6 +71,11 @@ def __init__(
7171
"/estimated_state", StateVector, self.estimator_callback
7272
)
7373

74+
self.control_pub = rospy.Publisher("/controller_command",
75+
Command,
76+
queue_size=1,
77+
)
78+
7479
if timeout:
7580
tstart = time.time()
7681
while not self.active and time.time() - tstart < timeout:

0 commit comments

Comments
 (0)