Skip to content

Commit d588548

Browse files
[Dynamics] Drone dynamics (#83)
* Add drone dynamics * VMAS-1.4.0 * Update vmas/simulator/dynamics/drone.py Co-authored-by: Matteo Bettini <55539777+matteobettini@users.noreply.github.com> * Update drone.py 1. the mass is obtained from self.agent.mass now 2. Updated how the drone_state is initialized * Update torque (action input) with correct indexing * implemented reset function * Update drone_state dimension * removed vmas_state * Bug fix The agent now behaves correctly using the interactive rendering. * Adding need_reset function * state is unpacked correctly now in def f * Amend * empty * Amend * Amend --------- Co-authored-by: Matteo Bettini <mb2389@cl.cam.ac.uk> Co-authored-by: Matteo Bettini <55539777+matteobettini@users.noreply.github.com>
1 parent a883c90 commit d588548

6 files changed

Lines changed: 286 additions & 12 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ customizable. Examples are: drag, friction, gravity, simulation timestep, non-di
246246
- **Agent actions**: Agents' physical actions are 2D forces for holonomic motion. Agent rotation can also be controlled through a torque action (activated by setting `agent.action.u_rot_range` at agent creation time). Agents can also be equipped with continuous or discrete communication actions.
247247
- **Action preprocessing**: By implementing the `process_action` function of a scenario, you can modify the agents' actions before they are passed to the simulator. This is used in `controllers` (where we provide different types of controllers to use) and `dynamics` (where we provide custom robot dynamic models).
248248
- **Controllers**: Controllers are components that can be appended to the neural network policy or replace it completely. We provide a `VelocityController` which can be used to treat input actions as velocities (instead of default vmas input forces). This PID controller takes velocities and outputs the forces which are fed to the simulator. See the `vel_control` debug scenario for an example.
249-
- **Dynamic models**: VMAS simulates holonomic dynamics models by default. Custom dynamics can be chosen at agent creation time. Implementations now include `DiffDriveDynamics` for differential drive robots and `KinematicBicycleDynamics` for kinematic bicycle model. See `diff_drive` and `kinematic_bicycle` debug scenarios for examples.
249+
- **Dynamic models**: VMAS simulates holonomic dynamics models by default. Custom dynamics can be chosen at agent creation time. Implementations now include `DiffDriveDynamics` for differential drive robots, `KinematicBicycleDynamics` for kinematic bicycle model, and `Drone` for quadcopter dynamics. See `diff_drive`, `kinematic_bicycle` and `drone` debug scenarios for examples.
250250
- **Differentiable**: By setting `grad_enabled=True` when creating an environment, the simulator will be differentiable, allowing gradients flowing through any of its function.
251251

252252
## Creating a new scenario
@@ -380,6 +380,7 @@ To create a fake screen you need to have `Xvfb` installed.
380380
| `circle_trajectory.py` | One agent is rewarded to move in a circle trajectory at the `desired_radius`. | <img src="https://github.com/matteobettini/vmas-media/blob/main/media/scenarios/circle_trajectory.gif?raw=true" alt="drawing" width="300"/> |
381381
| `diff_drive.py` | An example of the `diff_drive` dynamic model constraint. Both agents have rotational actions which can be controlled interactively. The first agent has differential drive dynamics. The second agent has standard vmas holonomic dynamics. | <img src="https://github.com/matteobettini/vmas-media/blob/main/media/scenarios/diff_drive.gif?raw=true" alt="drawing" width="300"/> |
382382
| `kinematic_bicycle.py` | An example of `kinematic_bicycle` dynamic model constraint. Both agents have rotational actions which can be controlled interactively. The first agent has kinematic bicycle model dynamics. The second agent has standard vmas holonomic dynamics. | <img src="https://github.com/matteobettini/vmas-media/blob/main/media/scenarios/kinematic_bicycle.gif?raw=true" alt="drawing" width="300"/> |
383+
| `drone.py` | An example of the `drone` dynamic model. | <img src="https://github.com/matteobettini/vmas-media/blob/main/media/scenarios/drone.gif?raw=true" alt="drawing" width="300"/> |
383384

384385
### [MPE](https://github.com/openai/multiagent-particle-envs)
385386

@@ -408,7 +409,7 @@ To create a fake screen you need to have `Xvfb` installed.
408409
- [ ] Improve test efficiency and add new tests
409410
- [ ] Implement 1D camera sensor
410411
- [ ] Implement 2D birds eye view camera sensor
411-
- [ ] Implement 2D drone dynamics
412+
- [X] Implement 2D drone dynamics
412413
- [X] Allow any number of actions
413414
- [X] Improve VMAS performance
414415
- [X] Dict obs support in torchrl

vmas/interactive_rendering.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ class InteractiveEnv:
3131
3232
You can change agent by pressing TAB
3333
You can reset the environment by pressing R
34-
You can move agents with the arrow keys and if the agent has a rotational action you can control it with M, N
35-
If you have more than 1 agent, you can control another one with W,A,S,D and Q,E for eventual rotational actions
34+
You can control agent actions with the arrow keys and M/N (left/right control the first action, up/down control the second, M/N controls the third)
35+
If you have more than 1 agent, you can control another one with W,A,S,D and Q,E in the same way.
3636
and switch the agent with these controls using LSHIFT
3737
"""
3838

@@ -305,8 +305,8 @@ def render_interactively(
305305
306306
You can change agent by pressing TAB
307307
You can reset the environment by pressing R
308-
You can move agents with the arrow keys and if the agent has a rotational action you can control it with M, N
309-
If you have more than 1 agent, you can control another one with W,A,S,D and Q,E for eventual rotational actions
308+
You can control agent actions with the arrow keys and M/N (left/right control the first action, up/down control the second, M/N controls the third)
309+
If you have more than 1 agent, you can control another one with W,A,S,D and Q,E in the same way.
310310
and switch the agent with these controls using LSHIFT
311311
"""
312312

@@ -333,8 +333,8 @@ def render_interactively(
333333
#
334334
# You can change agent by pressing TAB
335335
# You can reset the environment by pressing R
336-
# You can move agents with the arrow keys and if the agent has a rotational action you can control it with M, N
337-
# If you have more than 1 agent, you can control another one with W,A,S,D and Q,E for eventual rotational actions
336+
# You can control agent actions with the arrow keys and M/N (left/right control the first action, up/down control the second, M/N controls the third)
337+
# If you have more than 1 agent, you can control another one with W,A,S,D and Q,E in the same way.
338338
# and switch the agent with these controls using LSHIFT
339339

340340
scenario_name = "waterfall"

vmas/scenarios/debug/drone.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Copyright (c) 2024.
2+
# ProrokLab (https://www.proroklab.org/)
3+
# All rights reserved.
4+
5+
import typing
6+
from typing import List
7+
8+
import torch
9+
10+
from vmas import render_interactively
11+
from vmas.simulator.core import Agent, World
12+
from vmas.simulator.dynamics.drone import Drone
13+
from vmas.simulator.scenario import BaseScenario
14+
from vmas.simulator.utils import Color, ScenarioUtils
15+
16+
if typing.TYPE_CHECKING:
17+
from vmas.simulator.rendering import Geom
18+
19+
20+
class Scenario(BaseScenario):
21+
def make_world(self, batch_dim: int, device: torch.device, **kwargs):
22+
"""
23+
Drone example scenario
24+
Run this file to try it out.
25+
26+
You can control the three input torques using left/right arrows, up/down arrows, and m/n.
27+
"""
28+
self.plot_grid = True
29+
self.n_agents = kwargs.get("n_agents", 2)
30+
31+
# Make world
32+
world = World(batch_dim, device, substeps=10)
33+
34+
for i in range(self.n_agents):
35+
agent = Agent(
36+
name=f"drone_{i}",
37+
collide=True,
38+
render_action=True,
39+
u_range=[0.00001, 0.00001, 0.00001], # torque_x, torque_y, torque_z
40+
u_multiplier=[1, 1, 1],
41+
action_size=3, # We feed only the torque actions to interactively control the drone in the debug scenario
42+
# In non-debug cases, remove this line and the `process_action` function in this file
43+
dynamics=Drone(world, integration="rk4"),
44+
)
45+
world.add_agent(agent)
46+
47+
return world
48+
49+
def reset_world_at(self, env_index: int = None):
50+
ScenarioUtils.spawn_entities_randomly(
51+
self.world.agents,
52+
self.world,
53+
env_index,
54+
min_dist_between_entities=0.1,
55+
x_bounds=(-1, 1),
56+
y_bounds=(-1, 1),
57+
)
58+
59+
def reward(self, agent: Agent):
60+
return torch.zeros(self.world.batch_dim, device=self.world.device)
61+
62+
def process_action(self, agent: Agent):
63+
torque = agent.action.u
64+
thrust = torch.full(
65+
(self.world.batch_dim, 1),
66+
agent.mass * agent.dynamics.g,
67+
device=self.world.device,
68+
) # Add a fixed thrust to make sure the agent is not falling
69+
agent.action.u = torch.cat([thrust, torque], dim=-1)
70+
71+
def observation(self, agent: Agent):
72+
observations = [
73+
agent.state.pos,
74+
agent.state.vel,
75+
]
76+
return torch.cat(
77+
observations,
78+
dim=-1,
79+
)
80+
81+
def done(self):
82+
return torch.any(
83+
torch.stack(
84+
[agent.dynamics.needs_reset() for agent in self.world.agents], dim=-1
85+
),
86+
dim=-1,
87+
)
88+
89+
def extra_render(self, env_index: int = 0) -> "List[Geom]":
90+
from vmas.simulator import rendering
91+
92+
geoms: List[Geom] = []
93+
94+
# Agent rotation
95+
for agent in self.world.agents:
96+
color = Color.BLACK.value
97+
line = rendering.Line(
98+
(0, 0),
99+
(0.1, 0),
100+
width=1,
101+
)
102+
xform = rendering.Transform()
103+
xform.set_rotation(agent.state.rot[env_index])
104+
xform.set_translation(*agent.state.pos[env_index])
105+
line.add_attr(xform)
106+
line.set_color(*color)
107+
geoms.append(line)
108+
109+
return geoms
110+
111+
112+
if __name__ == "__main__":
113+
render_interactively(__file__, control_two_agents=True)

vmas/simulator/dynamics/common.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,15 @@ def agent(self):
2929
def agent(self, value):
3030
if self._agent is not None:
3131
raise ValueError("Agent in dynamics has already been set")
32-
if value.action_size < self.needed_action_size:
32+
self._agent = value
33+
34+
def check_and_process_action(self):
35+
action = self.agent.action.u
36+
if action.shape[1] < self.needed_action_size:
3337
raise ValueError(
34-
f"Agent action size {value.action_size} is less than the required dynamics action size {self.needed_action_size}"
38+
f"Agent action size {action.shape[1]} is less than the required dynamics action size {self.needed_action_size}"
3539
)
36-
self._agent = value
40+
self.process_action()
3741

3842
@property
3943
@abc.abstractmethod

vmas/simulator/dynamics/drone.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# Copyright (c) 2024.
2+
# ProrokLab (https://www.proroklab.org/)
3+
# All rights reserved.
4+
5+
from typing import Union
6+
7+
import torch
8+
from torch import Tensor
9+
10+
import vmas.simulator.core
11+
import vmas.simulator.utils
12+
from vmas.simulator.dynamics.common import Dynamics
13+
14+
15+
class Drone(Dynamics):
16+
def __init__(
17+
self,
18+
world: vmas.simulator.core.World,
19+
I_xx: float = 8.1e-3,
20+
I_yy: float = 8.1e-3,
21+
I_zz: float = 14.2e-3,
22+
integration: str = "rk4",
23+
):
24+
super().__init__()
25+
26+
assert integration in (
27+
"rk4",
28+
"euler",
29+
)
30+
31+
self.integration = integration
32+
self.I_xx = I_xx
33+
self.I_yy = I_yy
34+
self.I_zz = I_zz
35+
self.world = world
36+
self.g = 9.81
37+
self.dt = world.dt
38+
self.reset()
39+
40+
def reset(self, index: Union[Tensor, int] = None):
41+
if index is None:
42+
# Drone state: phi(roll), theta (pitch), psi (yaw),
43+
# p (roll_rate), q (pitch_rate), r (yaw_rate),
44+
# x_dot (vel_x), y_dot (vel_y), z_dot (vel_z),
45+
# x (pos_x), y (pos_y), z (pos_z)
46+
self.drone_state = torch.zeros(
47+
self.world.batch_dim,
48+
12,
49+
device=self.world.device,
50+
)
51+
else:
52+
self.drone_state[index] = 0.0
53+
54+
def needs_reset(self) -> Tensor:
55+
# Constraint roll and pitch within +-30 degrees
56+
return torch.any(self.drone_state[:, :2].abs() > 30 * (torch.pi / 180), dim=-1)
57+
58+
def euler(self, f, state):
59+
return state + self.dt * f(state)
60+
61+
def runge_kutta(self, f, state):
62+
k1 = f(state)
63+
k2 = f(state + self.dt * k1 / 2)
64+
k3 = f(state + self.dt * k2 / 2)
65+
k4 = f(state + self.dt * k3)
66+
return state + (self.dt / 6) * (k1 + 2 * k2 + 2 * k3 + k4)
67+
68+
@property
69+
def needed_action_size(self) -> int:
70+
return 4
71+
72+
def process_action(self):
73+
u = self.agent.action.u
74+
thrust = u[:, 0] # Thrust, sum of all propeller thrusts
75+
torque = u[:, 1:4] # Torque in x, y, z direction
76+
77+
thrust += self.agent.mass * self.g # Ensure the drone is not falling
78+
79+
self.drone_state[:, 9] = self.agent.state.pos[:, 0] # x
80+
self.drone_state[:, 10] = self.agent.state.pos[:, 1] # y
81+
self.drone_state[:, 2] = self.agent.state.rot[:, 0] # psi (yaw)
82+
83+
def f(state):
84+
phi = state[:, 0]
85+
theta = state[:, 1]
86+
psi = state[:, 2]
87+
p = state[:, 3]
88+
q = state[:, 4]
89+
r = state[:, 5]
90+
x_dot = state[:, 6]
91+
y_dot = state[:, 7]
92+
z_dot = state[:, 8]
93+
94+
c_phi = torch.cos(phi)
95+
s_phi = torch.sin(phi)
96+
c_theta = torch.cos(theta)
97+
s_theta = torch.sin(theta)
98+
c_psi = torch.cos(psi)
99+
s_psi = torch.sin(psi)
100+
101+
# Postion Dynamics
102+
x_ddot = (
103+
(c_phi * s_theta * c_psi + s_phi * s_psi) * thrust / self.agent.mass
104+
)
105+
y_ddot = (
106+
(c_phi * s_theta * s_psi - s_phi * c_psi) * thrust / self.agent.mass
107+
)
108+
z_ddot = (c_phi * c_theta) * thrust / self.agent.mass - self.g
109+
# Angular velocity dynamics
110+
p_dot = (torque[:, 0] - (self.I_yy - self.I_zz) * q * r) / self.I_xx
111+
q_dot = (torque[:, 1] - (self.I_zz - self.I_xx) * p * r) / self.I_yy
112+
r_dot = (torque[:, 2] - (self.I_xx - self.I_yy) * p * q) / self.I_zz
113+
114+
return torch.stack(
115+
[
116+
p,
117+
q,
118+
r,
119+
p_dot,
120+
q_dot,
121+
r_dot,
122+
x_ddot,
123+
y_ddot,
124+
z_ddot,
125+
x_dot,
126+
y_dot,
127+
z_dot,
128+
],
129+
dim=-1,
130+
)
131+
132+
if self.integration == "euler":
133+
new_drone_state = self.euler(f, self.drone_state)
134+
else:
135+
new_drone_state = self.runge_kutta(f, self.drone_state)
136+
137+
# Calculate the change in state
138+
delta_state = new_drone_state - self.drone_state
139+
self.drone_state = new_drone_state
140+
141+
# Calculate the accelerations required to achieve the change in state
142+
acceleration_x = delta_state[:, 6] / self.dt
143+
acceleration_y = delta_state[:, 7] / self.dt
144+
angular_acceleration = delta_state[:, 5] / self.dt
145+
146+
# Calculate the forces required for the linear accelerations
147+
force_x = self.agent.mass * acceleration_x
148+
force_y = self.agent.mass * acceleration_y
149+
150+
# Calculate the torque required for the angular acceleration
151+
torque_yaw = self.agent.moment_of_inertia * angular_acceleration
152+
153+
# Update the physical force and torque required for the user inputs
154+
self.agent.state.force[:, vmas.simulator.utils.X] = force_x
155+
self.agent.state.force[:, vmas.simulator.utils.Y] = force_y
156+
self.agent.state.torque = torque_yaw.unsqueeze(-1)

vmas/simulator/scenario.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def env_process_action(self, agent: Agent):
6565
agent.action_callback(self.world)
6666
# Customizable action processor
6767
self.process_action(agent)
68-
agent.dynamics.process_action()
68+
agent.dynamics.check_and_process_action()
6969

7070
@abstractmethod
7171
def make_world(self, batch_dim: int, device: torch.device, **kwargs) -> World:

0 commit comments

Comments
 (0)