Skip to content

Commit 73b979d

Browse files
committed
[wip,broken] Switch to crazyflow sim
1 parent 121217e commit 73b979d

File tree

5 files changed

+108
-158
lines changed

5 files changed

+108
-158
lines changed

config/level0.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ practice_without_track_objects = false
2424
# "pyb_dw": PyBullet with downwash
2525
# "pyb_gnd_drag_dw": PyBullet with ground effect, drag, and downwash.
2626
# "sys_id": System identification model. Only supported for attitude control interface (DroneRacingThrust-v0)
27-
physics = "pyb"
27+
physics = "analytical"
2828

2929
camera_view = [5.0, -40.0, -40.0, 0.5, -1.0, 0.5]
3030
sim_freq = 500 # Simulation frequency, in Hz
31-
ctrl_freq = 500 # Controller frequency, in Hz. This frequency is used to simulate the onboard controller, NOT for the environment's step function
31+
attitude_freq = 500 # Controller frequency, in Hz. This frequency is used to simulate the onboard controller, NOT for the environment's step function
3232
gui = false # Enable/disable PyBullet's GUI
3333

3434
[sim.disturbances.action]
@@ -44,7 +44,7 @@ high = [0.1, 0.1, 0.1]
4444
id = "DroneRacing-v0" # Either "DroneRacing-v0" or "DroneRacingThrust-v0". If using "DroneRacingThrust-v0", the drone will use the thrust controller instead of the position controller.
4545
reseed = true # Whether to re-seed the random number generator between episodes
4646
seed = 1337 # Random seed
47-
freq = 60 # Frequency of the environment's step function, in Hz
47+
freq = 50 # Frequency of the environment's step function, in Hz
4848
symbolic = false # Whether to include symbolic expressions in the info dict. Note: This can interfere with multiprocessing! If you want to parallelize your training, set this to false.
4949
sensor_range = 0.45 # Range at which the exact location of gates and obstacles become visible to the drone. Objects that are not in the drone's sensor range report their nominal position.
5050

@@ -78,4 +78,4 @@ pos = [0.0, 1.0, 1.4]
7878
pos = [1.0, 1.0, 0.05]
7979
rpy = [0, 0, 0]
8080
vel = [0, 0, 0]
81-
ang_vel = [0, 0, 0]
81+
rpy_rates = [0, 0, 0]

lsy_drone_racing/control/ppo_controller.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
based on the current observation.
55
66
.. note::
7-
You need to install the
7+
You need to install the
88
`stable-baselines3 <https://stable-baselines3.readthedocs.io/en/master/>`_ library to use this
99
controller.
1010
"""
@@ -88,9 +88,9 @@ def obs_transform(
8888
obs["vel"],
8989
obs["ang_vel"],
9090
gates_pose,
91-
obs["gates_in_range"],
91+
obs["gates_visited"],
9292
obs["obstacles_pos"].flatten(),
93-
obs["obstacles_in_range"],
93+
obs["obstacles_visited"],
9494
]
9595
)
9696
obs = np.concatenate(

lsy_drone_racing/envs/drone_racing_env.py

+97-124
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,16 @@
2727

2828
import copy as copy
2929
import logging
30-
import time
3130
from typing import TYPE_CHECKING
3231

3332
import gymnasium
3433
import numpy as np
34+
from crazyflow import Sim
3535
from gymnasium import spaces
3636
from scipy.spatial.transform import Rotation as R
3737

38-
from lsy_drone_racing.control.closing_controller import ClosingController
38+
from lsy_drone_racing.sim.noise import NoiseList
3939
from lsy_drone_racing.sim.physics import PhysicsMode
40-
from lsy_drone_racing.sim.sim import Sim
4140
from lsy_drone_racing.utils import check_gate_pass
4241

4342
if TYPE_CHECKING:
@@ -81,8 +80,6 @@ class DroneRacingEnv(gymnasium.Env):
8180
low-level controller.
8281
"""
8382

84-
CONTROLLER = "mellinger" # specifies controller type
85-
8683
def __init__(self, config: dict):
8784
"""Initialize the DroneRacingEnv.
8885
@@ -92,18 +89,20 @@ def __init__(self, config: dict):
9289
super().__init__()
9390
self.config = config
9491
self.sim = Sim(
95-
track=config.env.track,
96-
sim_freq=config.sim.sim_freq,
97-
ctrl_freq=config.sim.ctrl_freq,
98-
disturbances=getattr(config.sim, "disturbances", {}),
99-
randomization=getattr(config.env, "randomization", {}),
100-
gui=config.sim.gui,
92+
n_worlds=1,
10193
n_drones=1,
10294
physics=config.sim.physics,
95+
control="state",
96+
freq=config.sim.sim_freq,
97+
state_freq=config.env.freq,
98+
attitude_freq=config.sim.attitude_freq,
99+
rng_key=config.env.seed,
103100
)
104-
self.sim.seed(config.env.seed)
101+
self.contact_mask = np.array([0], dtype=bool)
102+
if config.sim.sim_freq % config.env.freq != 0:
103+
raise ValueError(f"({config.sim.sim_freq=}) is no multiple of ({config.env.freq=})")
105104
self.action_space = spaces.Box(low=-1, high=1, shape=(13,))
106-
n_gates, n_obstacles = len(self.sim.gates), len(self.sim.obstacles)
105+
n_gates, n_obstacles = len(config.env.track.gates), len(config.env.track.obstacles)
107106
self.observation_space = spaces.Dict(
108107
{
109108
"pos": spaces.Box(low=-np.inf, high=np.inf, shape=(3,)),
@@ -120,10 +119,22 @@ def __init__(self, config: dict):
120119
),
121120
}
122121
)
122+
rpy_max = np.array([85 / 180 * np.pi, 85 / 180 * np.pi, np.pi], np.float32) # Yaw unbounded
123+
pos_low, pos_high = np.array([-3, -3, 0]), np.array([3, 3, 2.5])
124+
self.state_space = spaces.Dict(
125+
{
126+
"pos": spaces.Box(low=pos_low, high=pos_high, dtype=np.float64),
127+
"rpy": spaces.Box(low=-rpy_max, high=rpy_max, dtype=np.float64),
128+
"vel": spaces.Box(low=-np.inf, high=np.inf, shape=(3,), dtype=np.float64),
129+
"ang_vel": spaces.Box(low=-np.inf, high=np.inf, shape=(3,), dtype=np.float64),
130+
}
131+
)
123132
self.target_gate = 0
124133
self.symbolic = self.sim.symbolic() if config.env.symbolic else None
125134
self._steps = 0
126135
self._last_drone_pos = np.zeros(3)
136+
self.gates, self.obstacles, self.drone = self.load_track(config.env.track)
137+
self.disturbances = self.load_disturbances(config.env.get("disturbances", None))
127138

128139
self.gates_visited = np.array([False] * len(config.env.track.gates))
129140
self.obstacles_visited = np.array([False] * len(config.env.track.obstacles))
@@ -147,16 +158,20 @@ def reset(
147158
if seed is not None:
148159
self.sim.seed(seed)
149160
self.sim.reset()
161+
states = self.sim.data.states.replace(
162+
pos=self.drone["pos"].reshape((1, 1, 3)),
163+
quat=self.drone["quat"].reshape((1, 1, 4)),
164+
vel=self.drone["vel"].reshape((1, 1, 3)),
165+
rpy_rates=self.drone["rpy_rates"].reshape((1, 1, 3)),
166+
)
167+
self.sim.data = self.sim.data.replace(states=states)
150168
self.target_gate = 0
151169
self._steps = 0
152-
self.sim.drone.reset(self.sim.drone.pos, self.sim.drone.rpy, self.sim.drone.vel)
153-
self._last_drone_pos[:] = self.sim.drone.pos
154-
if self.sim.n_drones > 1:
155-
raise NotImplementedError("Firmware wrapper does not support multiple drones.")
170+
self._last_drone_pos[:] = self.sim.data.states.pos[0, 0]
156171
info = self.info
157-
info["sim_freq"] = self.config.sim.sim_freq
158-
info["low_level_ctrl_freq"] = self.config.sim.ctrl_freq
159-
info["drone_mass"] = self.sim.drone.nominal_params.mass
172+
info["sim_freq"] = self.sim.data.core.freq
173+
info["low_level_ctrl_freq"] = self.sim.data.controls.attitude_freq
174+
info["drone_mass"] = self.sim.default_data.params.mass[0, 0]
160175
info["env_freq"] = self.config.env.freq
161176
return self.obs, info
162177

@@ -177,83 +192,52 @@ def step(
177192
), "sys_id model not supported for full state control interface"
178193
action = action.astype(np.float64) # Drone firmware expects float64
179194
assert action.shape == self.action_space.shape, f"Invalid action shape: {action.shape}"
180-
pos, vel, acc, yaw, rpy_rate = action[:3], action[3:6], action[6:9], action[9], action[10:]
181-
self.sim.drone.full_state_cmd(pos, vel, acc, yaw, rpy_rate)
182-
collision = self._inner_step_loop()
183-
terminated = self.terminated or collision
184-
return self.obs, self.reward, terminated, False, self.info
185-
186-
def _inner_step_loop(self) -> bool:
187-
"""Run the desired action for multiple simulation sub-steps.
195+
self.sim.state_control(action.reshape((1, 1, 13)))
196+
self.sim.step(self.sim.freq // self.sim.control_freq)
197+
return self.obs, self.reward, self.terminated, False, self.info
188198

189-
The outer controller is called at a lower frequency than the firmware loop. Each environment
190-
step therefore consists of multiple simulation steps. At each simulation step, the
191-
lower-level controller is called to compute the thrust and attitude commands.
192-
193-
Returns:
194-
True if a collision occured at any point during the simulation steps, else False.
195-
"""
196-
thrust = self.sim.drone.desired_thrust
197-
collision = False
198-
while (
199-
self.sim.drone.tick / self.sim.drone.firmware_freq
200-
< (self._steps + 1) / self.config.env.freq
201-
):
202-
self.sim.step(thrust)
203-
self.target_gate += self.gate_passed()
204-
if self.target_gate == self.sim.n_gates:
205-
self.target_gate = -1
206-
collision |= bool(self.sim.collisions)
207-
pos, rpy, vel = self.sim.drone.pos, self.sim.drone.rpy, self.sim.drone.vel
208-
thrust = self.sim.drone.step_controller(pos, rpy, vel)[::-1]
209-
self.sim.drone.desired_thrust[:] = thrust
210-
self._last_drone_pos[:] = self.sim.drone.pos
211-
self._steps += 1
212-
return collision
199+
def render(self):
200+
"""Render the environment."""
201+
self.sim.render()
213202

214203
@property
215204
def obs(self) -> dict[str, NDArray[np.floating]]:
216205
"""Return the observation of the environment."""
217206
obs = {
218-
"pos": self.sim.drone.pos.astype(np.float32),
219-
"rpy": self.sim.drone.rpy.astype(np.float32),
220-
"vel": self.sim.drone.vel.astype(np.float32),
221-
"ang_vel": self.sim.drone.ang_vel.astype(np.float32),
207+
"pos": np.array(self.sim.data.states.pos[0, 0], dtype=np.float32),
208+
"rpy": R.from_quat(self.sim.data.states.quat[0, 0]).as_euler("xyz").astype(np.float32),
209+
"vel": np.array(self.sim.data.states.vel[0, 0], dtype=np.float32),
210+
"ang_vel": np.array(self.sim.data.states.rpy_rates[0, 0], dtype=np.float32),
222211
}
223212
obs["ang_vel"][:] = R.from_euler("xyz", obs["rpy"]).apply(obs["ang_vel"], inverse=True)
224213

225-
gates = self.sim.gates
226-
obs["target_gate"] = self.target_gate if self.target_gate < len(gates) else -1
214+
obs["target_gate"] = self.target_gate if self.target_gate < len(self.gates) else -1
227215
# Add the gate and obstacle poses to the info. If gates or obstacles are in sensor range,
228216
# use the actual pose, otherwise use the nominal pose.
229-
in_range = self.sim.in_range(gates, self.sim.drone, self.config.env.sensor_range)
217+
drone_pos = self.sim.data.states.pos[0, 0]
218+
dist = np.linalg.norm(drone_pos[:2] - self.gates["nominal_pos"][:, :2])
219+
in_range = dist < self.config.env.sensor_range
230220
self.gates_visited = np.logical_or(self.gates_visited, in_range)
231221

232-
gates_pos = np.stack([g["nominal.pos"] for g in gates.values()])
233-
gates_pos[self.gates_visited] = np.stack([g["pos"] for g in gates.values()])[
234-
self.gates_visited
235-
]
236-
gates_rpy = np.stack([g["nominal.rpy"] for g in gates.values()])
237-
gates_rpy[self.gates_visited] = np.stack([g["rpy"] for g in gates.values()])[
238-
self.gates_visited
239-
]
222+
gates_pos = self.gates["nominal_pos"].copy()
223+
gates_pos[self.gates_visited] = self.gates["pos"][self.gates_visited]
224+
gates_rpy = self.gates["nominal_rpy"].copy()
225+
gates_rpy[self.gates_visited] = self.gates["rpy"][self.gates_visited]
240226
obs["gates_pos"] = gates_pos.astype(np.float32)
241227
obs["gates_rpy"] = gates_rpy.astype(np.float32)
242228
obs["gates_visited"] = self.gates_visited
243229

244-
obstacles = self.sim.obstacles
245-
in_range = self.sim.in_range(obstacles, self.sim.drone, self.config.env.sensor_range)
230+
dist = np.linalg.norm(drone_pos[:2] - self.obstacles["nominal_pos"][:, :2])
231+
in_range = dist < self.config.env.sensor_range
246232
self.obstacles_visited = np.logical_or(self.obstacles_visited, in_range)
247233

248-
obstacles_pos = np.stack([o["nominal.pos"] for o in obstacles.values()])
249-
obstacles_pos[self.obstacles_visited] = np.stack([o["pos"] for o in obstacles.values()])[
250-
self.obstacles_visited
251-
]
234+
obstacles_pos = self.obstacles["nominal_pos"].copy()
235+
obstacles_pos[self.obstacles_visited] = self.obstacles["pos"][self.obstacles_visited]
252236
obs["obstacles_pos"] = obstacles_pos.astype(np.float32)
253237
obs["obstacles_visited"] = self.obstacles_visited
254238

255-
if "observation" in self.sim.disturbances:
256-
obs = self.sim.disturbances["observation"].apply(obs)
239+
if "observation" in self.disturbances:
240+
obs = self.disturbances["observation"].apply(obs)
257241
return obs
258242

259243
@property
@@ -278,11 +262,21 @@ def terminated(self) -> bool:
278262
True if the drone is out of bounds, colliding with an obstacle, or has passed all gates,
279263
else False.
280264
"""
281-
state = {k: getattr(self.sim.drone, k).copy() for k in self.sim.state_space}
282-
state["ang_vel"] = R.from_euler("xyz", state["rpy"]).apply(state["ang_vel"], inverse=True)
283-
if state not in self.sim.state_space:
265+
quat = self.sim.data.states.quat[0, 0]
266+
state = {
267+
"pos": np.array(self.sim.data.states.pos[0, 0], dtype=np.float32),
268+
"rpy": np.array(R.from_quat(quat).as_euler("xyz"), dtype=np.float32),
269+
"vel": np.array(self.sim.data.states.vel[0, 0], dtype=np.float32),
270+
"ang_vel": np.array(
271+
R.from_quat(quat).apply(self.sim.data.states.rpy_rates[0, 0], inverse=True),
272+
dtype=np.float32,
273+
),
274+
}
275+
if state not in self.state_space:
284276
return True # Drone is out of bounds
285-
if self.sim.collisions:
277+
if np.logical_and(self.sim.contacts("drone:0"), self.contact_mask).any():
278+
return True
279+
if self.sim.data.states.pos[0, 0, 2] < 0.0:
286280
return True
287281
if self.target_gate == -1: # Drone has passed all gates
288282
return True
@@ -291,7 +285,30 @@ def terminated(self) -> bool:
291285
@property
292286
def info(self) -> dict:
293287
"""Return an info dictionary containing additional information about the environment."""
294-
return {"collisions": self.sim.collisions, "symbolic_model": self.symbolic}
288+
return {"collisions": self.sim.contacts("drone:0"), "symbolic_model": self.symbolic}
289+
290+
def load_track(self, track: dict) -> tuple[dict, dict, dict]:
291+
"""Load the track from the config file."""
292+
gate_pos = np.array([g["pos"] for g in track.gates])
293+
gate_rpy = np.array([g["rpy"] for g in track.gates])
294+
gates = {"pos": gate_pos, "rpy": gate_rpy, "nominal_pos": gate_pos, "nominal_rpy": gate_rpy}
295+
obstacle_pos = np.array([o["pos"] for o in track.obstacles])
296+
obstacles = {"pos": obstacle_pos, "nominal_pos": obstacle_pos}
297+
drone = {
298+
k: np.array([track.drone.get(k)], dtype=np.float32)
299+
for k in ("pos", "rpy", "vel", "rpy_rates")
300+
}
301+
drone["quat"] = R.from_euler("xyz", drone["rpy"]).as_quat()
302+
return gates, obstacles, drone
303+
304+
def load_disturbances(self, disturbances: dict | None = None) -> dict:
305+
"""Load the disturbances from the config."""
306+
dist = {}
307+
if disturbances is None: # Default: no passive disturbances.
308+
return dist
309+
for mode, spec in disturbances.items():
310+
dist[mode] = NoiseList.from_specs([spec])
311+
return dist
295312

296313
def gate_passed(self) -> bool:
297314
"""Check if the drone has passed a gate.
@@ -310,50 +327,6 @@ def gate_passed(self) -> bool:
310327

311328
def close(self):
312329
"""Close the environment by stopping the drone and landing back at the starting position."""
313-
return_home = True # makes the drone simulate the return to home after stopping
314-
315-
if return_home:
316-
# This is done to run the closing controller at a different frequency than the controller before
317-
# Does not influence other code, since this part is already in closing!
318-
# WARNING: When changing the frequency, you must also change the current _step!!!
319-
freq_new = 100 # Hz
320-
self._steps = int(self._steps / self.config.env.freq * freq_new)
321-
self.config.env.freq = freq_new
322-
t_step_ctrl = 1 / self.config.env.freq
323-
324-
obs = self.obs
325-
obs["acc"] = np.array(
326-
[0, 0, 0]
327-
) # TODO, use actual value when avaiable or do one step to calculate from velocity
328-
info = self.info
329-
info["env_freq"] = self.config.env.freq
330-
info["drone_start_pos"] = self.config.env.track.drone.pos
331-
332-
controller = ClosingController(obs, info)
333-
t_total = controller.t_total
334-
335-
for i in np.arange(int(t_total / t_step_ctrl)): # hover for some more time
336-
action = controller.compute_control(obs)
337-
action = action.astype(np.float64) # Drone firmware expects float64
338-
if self.config.sim.physics == PhysicsMode.SYS_ID:
339-
print("[Warning] sys_id model not supported by return home script")
340-
break
341-
pos, vel, acc, yaw, rpy_rate = (
342-
action[:3],
343-
action[3:6],
344-
action[6:9],
345-
action[9],
346-
action[10:],
347-
)
348-
self.sim.drone.full_state_cmd(pos, vel, acc, yaw, rpy_rate)
349-
collision = self._inner_step_loop()
350-
terminated = self.terminated or collision
351-
obs = self.obs
352-
obs["acc"] = np.array([0, 0, 0])
353-
controller.step_callback(action, obs, self.reward, terminated, False, info)
354-
if self.config.sim.gui: # Only sync if gui is active
355-
time.sleep(t_step_ctrl)
356-
357330
self.sim.close()
358331

359332

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ dependencies = [
2626
"PyYAML >= 6.0.1",
2727
"rospkg >= 1.5.1",
2828
"scipy >= 1.10.1",
29-
"gymnasium == 0.29.1",
29+
"gymnasium >= 1.0.0",
3030
"toml >= 0.10.2",
3131
"casadi >= 3.6.5",
3232
"swig >= 4.2.1", # Required for pycffirmware

0 commit comments

Comments
 (0)