cbfpy 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cbfpy/__init__.py +11 -0
- cbfpy/cbfs/__init__.py +0 -0
- cbfpy/cbfs/cbf.py +384 -0
- cbfpy/cbfs/clf_cbf.py +490 -0
- cbfpy/config/__init__.py +0 -0
- cbfpy/config/cbf_config.py +401 -0
- cbfpy/config/clf_cbf_config.py +251 -0
- cbfpy/envs/__init__.py +0 -0
- cbfpy/envs/arm_envs.py +84 -0
- cbfpy/envs/base_env.py +69 -0
- cbfpy/envs/car_env.py +332 -0
- cbfpy/envs/drone_env.py +153 -0
- cbfpy/envs/point_robot_envs.py +209 -0
- cbfpy/examples/__init__.py +0 -0
- cbfpy/examples/adaptive_cruise_control_demo.py +117 -0
- cbfpy/examples/drone_demo.py +109 -0
- cbfpy/examples/joint_limits_demo.py +150 -0
- cbfpy/examples/point_robot_demo.py +91 -0
- cbfpy/examples/point_robot_obstacle_demo.py +118 -0
- cbfpy/temp/test_import.py +3 -0
- cbfpy/utils/__init__.py +0 -0
- cbfpy/utils/general_utils.py +131 -0
- cbfpy/utils/jax_utils.py +26 -0
- cbfpy/utils/math_utils.py +21 -0
- cbfpy/utils/visualization.py +93 -0
- cbfpy-0.0.1.dist-info/LICENSE +21 -0
- cbfpy-0.0.1.dist-info/METADATA +226 -0
- cbfpy-0.0.1.dist-info/RECORD +33 -0
- cbfpy-0.0.1.dist-info/WHEEL +5 -0
- cbfpy-0.0.1.dist-info/top_level.txt +2 -0
- test/__init__.py +0 -0
- test/test_speed.py +191 -0
- test/test_utils.py +34 -0
cbfpy/envs/arm_envs.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Simulation environents for robot arms
|
|
3
|
+
|
|
4
|
+
This currently includes a very simple 3-DOF environment which helps demonstrate joint limit avoidance,
|
|
5
|
+
but more will be added in the future
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pybullet
|
|
10
|
+
from pybullet_utils.bullet_client import BulletClient
|
|
11
|
+
|
|
12
|
+
from cbfpy.utils.general_utils import find_assets_dir, stdout_redirected
|
|
13
|
+
from cbfpy.envs.base_env import BaseEnv
|
|
14
|
+
|
|
15
|
+
URDF = find_assets_dir() + "three_dof_arm.urdf"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class JointLimitsEnv(BaseEnv):
|
|
19
|
+
"""Simulation environment for the 3-DOF arm joint-limit-avoidance demo
|
|
20
|
+
|
|
21
|
+
This includes a desired reference trajectory which is unsafe: it will command sinusoidal
|
|
22
|
+
joint motions (with different frequencies per link) that will exceed the joint limits of the robot
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
with stdout_redirected(restore=False):
|
|
27
|
+
self.client: pybullet = BulletClient(pybullet.GUI)
|
|
28
|
+
self.robot = pybullet.loadURDF(URDF, useFixedBase=True)
|
|
29
|
+
self.num_joints = self.client.getNumJoints(self.robot)
|
|
30
|
+
self.q_min = np.array(
|
|
31
|
+
[self.client.getJointInfo(self.robot, i)[8] for i in range(self.num_joints)]
|
|
32
|
+
)
|
|
33
|
+
self.q_max = np.array(
|
|
34
|
+
[self.client.getJointInfo(self.robot, i)[9] for i in range(self.num_joints)]
|
|
35
|
+
)
|
|
36
|
+
self.timestep = self.client.getPhysicsEngineParameters()["fixedTimeStep"]
|
|
37
|
+
self.t = 0
|
|
38
|
+
|
|
39
|
+
# Sinusoids for the desired joint positions
|
|
40
|
+
# Setting the amplitude to be the full joint range means we will command DOUBLE
|
|
41
|
+
# the joint range, exceeding our limits
|
|
42
|
+
self.omegas = 0.1 * np.array([1.0, 2.0, 3.0])
|
|
43
|
+
self.amps = self.q_max - self.q_min
|
|
44
|
+
self.offsets = np.zeros(3)
|
|
45
|
+
|
|
46
|
+
def step(self):
|
|
47
|
+
self.client.stepSimulation()
|
|
48
|
+
self.t += self.timestep
|
|
49
|
+
|
|
50
|
+
def get_state(self):
|
|
51
|
+
states = self.client.getJointStates(self.robot, range(self.num_joints))
|
|
52
|
+
return np.array([states[i][0] for i in range(self.num_joints)])
|
|
53
|
+
|
|
54
|
+
def get_desired_state(self):
|
|
55
|
+
# Evaluate our unsafe sinusoidal trajectory
|
|
56
|
+
return self.amps * np.sin(self.omegas * self.t) + self.offsets
|
|
57
|
+
|
|
58
|
+
def apply_control(self, u):
|
|
59
|
+
self.client.setJointMotorControlArray(
|
|
60
|
+
self.robot,
|
|
61
|
+
list(range(self.num_joints)),
|
|
62
|
+
self.client.VELOCITY_CONTROL,
|
|
63
|
+
targetVelocities=u,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _test_env():
|
|
68
|
+
"""Test the environment behavior under an **unsafe** nominal controller"""
|
|
69
|
+
|
|
70
|
+
def nominal_controller(z, z_des):
|
|
71
|
+
k = 1.0
|
|
72
|
+
return k * (z_des - z)
|
|
73
|
+
|
|
74
|
+
env = JointLimitsEnv()
|
|
75
|
+
while True:
|
|
76
|
+
z = env.get_state()
|
|
77
|
+
z_des = env.get_desired_state()
|
|
78
|
+
u = nominal_controller(z, z_des)
|
|
79
|
+
env.apply_control(u)
|
|
80
|
+
env.step()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
_test_env()
|
cbfpy/envs/base_env.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Base Environment
|
|
3
|
+
|
|
4
|
+
This is a convenient structure for building demo environments to test CBFs. However, it is not necessary to use this.
|
|
5
|
+
|
|
6
|
+
For instance, going back to the CBF usage pseudocode,
|
|
7
|
+
```
|
|
8
|
+
while True:
|
|
9
|
+
z = get_state()
|
|
10
|
+
z_des = get_desired_state()
|
|
11
|
+
u_nom = nominal_controller(z, z_des)
|
|
12
|
+
u = cbf.safety_filter(z, u_nom)
|
|
13
|
+
apply_control(u)
|
|
14
|
+
step()
|
|
15
|
+
```
|
|
16
|
+
We use this base environment to set up the `get_state`, `get_desired_state`, `apply_control`, and `step` methods in
|
|
17
|
+
any derived environments.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
|
|
22
|
+
from jax.typing import ArrayLike
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BaseEnv(ABC):
|
|
26
|
+
"""Simulation environment Abstract Base Class for testing CBFs
|
|
27
|
+
|
|
28
|
+
Any environment inheriting from this class should implement the following methods:
|
|
29
|
+
|
|
30
|
+
- `step`: Run a single simulation step
|
|
31
|
+
- `get_state`: Get the current state of the robot
|
|
32
|
+
- `get_desired_state`: Get the desired state of the robot
|
|
33
|
+
- `apply_control`: Apply a control input to the robot
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def step(self) -> None:
|
|
38
|
+
"""Runs a single simulation step for the environment
|
|
39
|
+
|
|
40
|
+
This should update any dynamics and visuals
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def get_state(self) -> ArrayLike:
|
|
46
|
+
"""Returns the current state of the environment
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
ArrayLike: State, shape (n,)
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def get_desired_state(self) -> ArrayLike:
|
|
55
|
+
"""Returns the desired state of the environment
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
ArrayLike: Desired state, shape (n,)
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def apply_control(self, u: ArrayLike) -> None:
|
|
64
|
+
"""Applies the control input to the environment
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
u (ArrayLike): Control, shape (m,)
|
|
68
|
+
"""
|
|
69
|
+
pass
|
cbfpy/envs/car_env.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Adaptive Cruise Control Simulation Environment
|
|
3
|
+
|
|
4
|
+
Simple simulation of a leader-follower vehicle system for adaptive cruise control testing
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pygame
|
|
11
|
+
|
|
12
|
+
from cbfpy.envs.base_env import BaseEnv
|
|
13
|
+
|
|
14
|
+
CAR_ASCII = """
|
|
15
|
+
_______
|
|
16
|
+
// ||\\ \\
|
|
17
|
+
_____//___||_\\ \\___
|
|
18
|
+
) _ _ \\
|
|
19
|
+
|_/ \\________/ \\___|
|
|
20
|
+
___\\_/________\\_/______
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VehicleEnv(BaseEnv):
|
|
25
|
+
"""Leader/follower vehicle simulation environment for adaptive cruise control testing
|
|
26
|
+
|
|
27
|
+
This will bring up an interactive Pygame window where you can control the speed of the leader car,
|
|
28
|
+
and the follower car will be controlled by a simple adaptive cruise control algorithm.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
controller_name (str): Name of the controller being tested
|
|
32
|
+
mass (float, optional): Mass of the follower vehicle. Defaults to 1650.0 (kg)
|
|
33
|
+
drag_coeffs (Tuple[float, float, float]): Coefficients of a simple polynomial friction model, as
|
|
34
|
+
described in the Ames CBF paper. Defaults to (0.1, 5.0, 0.25)
|
|
35
|
+
v_des (float, optional): Desired velocity of the follower car. Defaults to 24.0 (m/s)
|
|
36
|
+
init_leader_pos (float, optional): Initial position of the leader car. Defaults to 0.0 (meters)
|
|
37
|
+
init_leader_vel (float, optional): Initial velocity of the leader car. Defaults to 14.0 (m/s)
|
|
38
|
+
init_follower_pos (float, optional): Initial position of the follower car. Defaults to -20.0 (meters)
|
|
39
|
+
init_follower_vel (float, optional): Initial velocity of the follower car. Defaults to 10.0 (m/s)
|
|
40
|
+
u_min (float, optional): Minimum control input, in Newtons. Defaults to -np.inf (unconstrained)
|
|
41
|
+
u_max (float, optional): Maximum control input, in Newtons. Defaults to np.inf (unconstrained)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Constants
|
|
45
|
+
PIXELS_PER_METER = 15
|
|
46
|
+
MIN_FOLLOW_DIST_M = 1
|
|
47
|
+
MIN_FOLLOW_DIST_PX = PIXELS_PER_METER * MIN_FOLLOW_DIST_M
|
|
48
|
+
|
|
49
|
+
# Screen dimensions
|
|
50
|
+
SCREEN_WIDTH = 600
|
|
51
|
+
SCREEN_HEIGHT = 800
|
|
52
|
+
|
|
53
|
+
# Colors
|
|
54
|
+
WHITE = (255, 255, 255)
|
|
55
|
+
GRAY = (100, 100, 100)
|
|
56
|
+
BLACK = (0, 0, 0)
|
|
57
|
+
BLUE = (0, 0, 255)
|
|
58
|
+
RED = (255, 0, 0)
|
|
59
|
+
YELLOW = (255, 255, 0)
|
|
60
|
+
|
|
61
|
+
# Road properties
|
|
62
|
+
DASH_HEIGHT_M = 2
|
|
63
|
+
DASH_WIDTH_M = 0.3
|
|
64
|
+
DASH_GAP_M = 1
|
|
65
|
+
DASH_HEIGHT_PX = PIXELS_PER_METER * DASH_HEIGHT_M
|
|
66
|
+
DASH_WIDTH_PX = PIXELS_PER_METER * DASH_WIDTH_M
|
|
67
|
+
DASH_GAP_PX = PIXELS_PER_METER * DASH_GAP_M
|
|
68
|
+
|
|
69
|
+
# Car properties
|
|
70
|
+
CAR_WIDTH_M = 2
|
|
71
|
+
CAR_HEIGHT_M = 4
|
|
72
|
+
CAR_WIDTH_PX = PIXELS_PER_METER * CAR_WIDTH_M
|
|
73
|
+
CAR_HEIGHT_PX = PIXELS_PER_METER * CAR_HEIGHT_M
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
controller_name: str,
|
|
78
|
+
mass: float = 1650.0,
|
|
79
|
+
drag_coeffs: Tuple[float, float, float] = (0.1, 5.0, 0.25),
|
|
80
|
+
v_des: float = 24.0,
|
|
81
|
+
init_leader_pos: float = 0.0,
|
|
82
|
+
init_leader_vel: float = 14.0,
|
|
83
|
+
init_follower_pos: float = -20.0,
|
|
84
|
+
init_follower_vel: float = 10.0,
|
|
85
|
+
u_min: float = -np.inf,
|
|
86
|
+
u_max: float = np.inf,
|
|
87
|
+
):
|
|
88
|
+
self.mass = mass
|
|
89
|
+
self.v_des = v_des # Desired velocity of the follower car
|
|
90
|
+
assert len(drag_coeffs) == 3
|
|
91
|
+
self.f0, self.f1, self.f2 = drag_coeffs
|
|
92
|
+
# Initialize Pygame
|
|
93
|
+
pygame.init()
|
|
94
|
+
# Set up the display
|
|
95
|
+
self.screen = pygame.display.set_mode(
|
|
96
|
+
(VehicleEnv.SCREEN_WIDTH, VehicleEnv.SCREEN_HEIGHT)
|
|
97
|
+
)
|
|
98
|
+
pygame.display.set_caption(f"Adaptive Cruise Control: {controller_name}")
|
|
99
|
+
# Create car sprites
|
|
100
|
+
self.leader_sprite = pygame.Surface(
|
|
101
|
+
(VehicleEnv.CAR_WIDTH_PX, VehicleEnv.CAR_HEIGHT_PX)
|
|
102
|
+
)
|
|
103
|
+
self.leader_sprite.fill(VehicleEnv.RED)
|
|
104
|
+
self.follower_sprite = pygame.Surface(
|
|
105
|
+
(VehicleEnv.CAR_WIDTH_PX, VehicleEnv.CAR_HEIGHT_PX)
|
|
106
|
+
)
|
|
107
|
+
self.follower_sprite.fill(VehicleEnv.BLUE)
|
|
108
|
+
# Positioning the cars in the display
|
|
109
|
+
self.leader_x = int(VehicleEnv.SCREEN_WIDTH / 4 - VehicleEnv.CAR_WIDTH_PX / 2)
|
|
110
|
+
self.leader_y = VehicleEnv.SCREEN_HEIGHT // 5
|
|
111
|
+
self.follower_x = self.leader_x
|
|
112
|
+
self.follower_y = self.leader_y + VehicleEnv.MIN_FOLLOW_DIST_PX
|
|
113
|
+
|
|
114
|
+
# Init dynamics
|
|
115
|
+
self.leader_pos = init_leader_pos
|
|
116
|
+
self.leader_vel = init_leader_vel
|
|
117
|
+
self.follower_pos = init_follower_pos
|
|
118
|
+
self.follower_vel = init_follower_vel
|
|
119
|
+
|
|
120
|
+
self.leader_vel_des = init_leader_vel
|
|
121
|
+
|
|
122
|
+
# Initial position of dashed lines
|
|
123
|
+
self.dash_offset = 0
|
|
124
|
+
|
|
125
|
+
self.font = pygame.font.SysFont("Arial", 20)
|
|
126
|
+
|
|
127
|
+
self.fps = 60
|
|
128
|
+
self.dt = 1 / self.fps
|
|
129
|
+
self.running = True
|
|
130
|
+
|
|
131
|
+
self.last_control = 0.0
|
|
132
|
+
|
|
133
|
+
self.u_min = u_min
|
|
134
|
+
self.u_max = u_max
|
|
135
|
+
|
|
136
|
+
# Print instructions
|
|
137
|
+
print(CAR_ASCII)
|
|
138
|
+
print("Beginning Vehicle Simulation")
|
|
139
|
+
print("Press UP and DOWN to control the speed of the leader car")
|
|
140
|
+
print("Press ESC to quit")
|
|
141
|
+
|
|
142
|
+
def pixels_to_meters(self, n_pixels: int) -> float:
|
|
143
|
+
"""Helper function: Converts pixels to meters
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
n_pixels (int): Number of pixels
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
float: Distance in meters
|
|
150
|
+
"""
|
|
151
|
+
return n_pixels / self.PIXELS_PER_METER
|
|
152
|
+
|
|
153
|
+
def meters_to_pixels(self, n_meters: float) -> float:
|
|
154
|
+
"""Helper function: Converts meters to pixels
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
n_meters (float): Distance in meters
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
float: Number of pixels (Note: Not rounded to an integer value)
|
|
161
|
+
"""
|
|
162
|
+
return n_meters * self.PIXELS_PER_METER
|
|
163
|
+
|
|
164
|
+
def friction(self, v: float) -> float:
|
|
165
|
+
"""Computes the drag force on the vehicle with the simple model presented in the Ames CBF paper
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
v (float): Car speed, in m/s
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
float: Drag force on the car, in Newtons
|
|
172
|
+
"""
|
|
173
|
+
return self.f0 + self.f1 * v + self.f2 * v**2
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def follow_distance(self):
|
|
177
|
+
"""Distance between the leader and follower cars, in meters"""
|
|
178
|
+
return self.leader_pos - self.follower_pos - self.CAR_HEIGHT_M
|
|
179
|
+
|
|
180
|
+
def get_state(self):
|
|
181
|
+
return np.array(
|
|
182
|
+
[
|
|
183
|
+
self.follower_vel,
|
|
184
|
+
self.leader_vel,
|
|
185
|
+
self.follow_distance,
|
|
186
|
+
]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def get_desired_state(self):
|
|
190
|
+
leader_speed_kmh = 3.6 * self.leader_vel
|
|
191
|
+
safe_follow_distance = leader_speed_kmh / 2 + self.MIN_FOLLOW_DIST_M
|
|
192
|
+
# Note: This desired state is primarily for the simple nominal controller and NOT the CLF-CBF
|
|
193
|
+
return np.array(
|
|
194
|
+
[
|
|
195
|
+
self.v_des,
|
|
196
|
+
self.leader_vel,
|
|
197
|
+
safe_follow_distance,
|
|
198
|
+
]
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def apply_control(self, u) -> None:
|
|
202
|
+
u = np.clip(u, self.u_min, self.u_max).item()
|
|
203
|
+
if np.isnan(u):
|
|
204
|
+
print("Infeasible. Using last safe control")
|
|
205
|
+
u = self.last_control
|
|
206
|
+
force = u - self.friction(self.follower_vel)
|
|
207
|
+
follower_accel = force / self.mass
|
|
208
|
+
self.follower_vel += follower_accel * self.dt
|
|
209
|
+
self.follower_pos += self.follower_vel * self.dt
|
|
210
|
+
self.last_control = u
|
|
211
|
+
|
|
212
|
+
def leader_controller(self) -> float:
|
|
213
|
+
"""Simple controller for the leader car
|
|
214
|
+
|
|
215
|
+
The desired leader velocity will be set by the user, and this controller will try to track that velocity
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
float: Control input to the leader car: Acceleration force, in Newtons
|
|
219
|
+
"""
|
|
220
|
+
kv = 1000
|
|
221
|
+
return kv * (self.leader_vel_des - self.leader_vel)
|
|
222
|
+
|
|
223
|
+
def step(self):
|
|
224
|
+
# Handle events
|
|
225
|
+
# This includes where the speed of the main controlled by the user
|
|
226
|
+
for event in pygame.event.get():
|
|
227
|
+
if event.type == pygame.QUIT:
|
|
228
|
+
self.running = False
|
|
229
|
+
return
|
|
230
|
+
elif event.type == pygame.KEYDOWN:
|
|
231
|
+
if event.key == pygame.K_UP:
|
|
232
|
+
self.leader_vel_des += 1
|
|
233
|
+
elif event.key == pygame.K_DOWN:
|
|
234
|
+
self.leader_vel_des -= 1
|
|
235
|
+
elif event.key == pygame.K_ESCAPE:
|
|
236
|
+
self.running = False
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
leader_u = self.leader_controller()
|
|
240
|
+
leader_force = leader_u - self.friction(self.leader_vel)
|
|
241
|
+
# Assume both vehicles have the same mass
|
|
242
|
+
leader_accel = leader_force / self.mass
|
|
243
|
+
self.leader_vel += leader_accel * self.dt
|
|
244
|
+
self.leader_pos += self.leader_vel * self.dt
|
|
245
|
+
follow_dist = self.follow_distance
|
|
246
|
+
|
|
247
|
+
# Update locations of the cars in pixel frame
|
|
248
|
+
self.follower_y = (
|
|
249
|
+
self.leader_y + self.meters_to_pixels(follow_dist) + self.CAR_HEIGHT_PX
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Clear the screen
|
|
253
|
+
self.screen.fill(VehicleEnv.WHITE)
|
|
254
|
+
|
|
255
|
+
# Draw the road
|
|
256
|
+
pygame.draw.rect(
|
|
257
|
+
self.screen,
|
|
258
|
+
VehicleEnv.GRAY,
|
|
259
|
+
(
|
|
260
|
+
0,
|
|
261
|
+
0,
|
|
262
|
+
VehicleEnv.SCREEN_WIDTH // 2,
|
|
263
|
+
VehicleEnv.SCREEN_HEIGHT,
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Draw dashed lines on the road
|
|
268
|
+
dash_y = self.dash_offset
|
|
269
|
+
while dash_y < VehicleEnv.SCREEN_HEIGHT:
|
|
270
|
+
pygame.draw.rect(
|
|
271
|
+
self.screen,
|
|
272
|
+
VehicleEnv.YELLOW,
|
|
273
|
+
(
|
|
274
|
+
VehicleEnv.SCREEN_WIDTH // 4 - VehicleEnv.DASH_WIDTH_PX // 2,
|
|
275
|
+
dash_y,
|
|
276
|
+
VehicleEnv.DASH_WIDTH_PX,
|
|
277
|
+
VehicleEnv.DASH_HEIGHT_PX,
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
dash_y += VehicleEnv.DASH_HEIGHT_PX + VehicleEnv.DASH_GAP_PX
|
|
281
|
+
|
|
282
|
+
# Move the dashed lines
|
|
283
|
+
self.dash_offset += self.leader_vel
|
|
284
|
+
if self.dash_offset >= VehicleEnv.DASH_HEIGHT_PX + VehicleEnv.DASH_GAP_PX:
|
|
285
|
+
self.dash_offset = 0
|
|
286
|
+
|
|
287
|
+
# Draw the cars
|
|
288
|
+
self.screen.blit(self.leader_sprite, (self.leader_x, self.leader_y))
|
|
289
|
+
self.screen.blit(self.follower_sprite, (self.follower_x, self.follower_y))
|
|
290
|
+
|
|
291
|
+
# Print info to the pygame window
|
|
292
|
+
info_1 = f"Leader desired speed: {self.leader_vel_des:.2f}"
|
|
293
|
+
info_2 = f"Leader speed: {self.leader_vel:.2f}"
|
|
294
|
+
info_3 = f"Follower speed: {self.follower_vel:.2f}"
|
|
295
|
+
info_4 = f"Follow distance: {follow_dist:.2f}"
|
|
296
|
+
info_5 = f"Control: {self.last_control:.2f}"
|
|
297
|
+
text = [info_1, info_2, info_3, info_4, info_5]
|
|
298
|
+
for i, line in enumerate(text):
|
|
299
|
+
self.screen.blit(
|
|
300
|
+
self.font.render(line, True, VehicleEnv.BLACK),
|
|
301
|
+
(VehicleEnv.SCREEN_WIDTH // 2 + 10, 10 + 30 * i),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Update the display
|
|
305
|
+
pygame.display.flip()
|
|
306
|
+
|
|
307
|
+
# Cap the frame rate
|
|
308
|
+
pygame.time.Clock().tick(self.fps)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _test_env():
|
|
312
|
+
"""Test the environment behavior under an **unsafe** nominal controller"""
|
|
313
|
+
|
|
314
|
+
def nominal_controller(z, z_des):
|
|
315
|
+
vf, vl, D = z
|
|
316
|
+
vf_des, vl_des, D_des = z_des
|
|
317
|
+
kp = 1000
|
|
318
|
+
kv = 1000
|
|
319
|
+
return kp * (D - D_des) + kv * (vf_des - vf)
|
|
320
|
+
|
|
321
|
+
env = VehicleEnv("Nominal Controller")
|
|
322
|
+
print("\nDemoing the vehicle environment with an unsafe controller")
|
|
323
|
+
while env.running:
|
|
324
|
+
z = env.get_state()
|
|
325
|
+
z_des = env.get_desired_state()
|
|
326
|
+
u = nominal_controller(z, z_des)
|
|
327
|
+
env.apply_control(u)
|
|
328
|
+
env.step()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
_test_env()
|
cbfpy/envs/drone_env.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Drone Environment
|
|
3
|
+
|
|
4
|
+
This is a wrapper around the gym-pybullet-drones environment, using velocity control.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pybullet
|
|
12
|
+
from pybullet_utils.bullet_client import BulletClient
|
|
13
|
+
from jax.typing import ArrayLike
|
|
14
|
+
from jax import Array
|
|
15
|
+
|
|
16
|
+
# Note: the original gym_pybullet_drones repo has a lot of dependencies that are not necessary for this demo.
|
|
17
|
+
# Use the fork at https://github.com/danielpmorton/gym-pybullet-drones instead
|
|
18
|
+
try:
|
|
19
|
+
from gym_pybullet_drones.envs.VelocityAviary import VelocityAviary
|
|
20
|
+
except ImportError as e:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
"Please install the forked version of gym-pybullet-drones:"
|
|
23
|
+
+ "\n'pip install 'gym_pybullet_drones @ git+https://github.com/danielpmorton/gym-pybullet-drones.git''"
|
|
24
|
+
) from e
|
|
25
|
+
|
|
26
|
+
from cbfpy.utils.visualization import visualize_3D_box
|
|
27
|
+
from cbfpy.envs.base_env import BaseEnv
|
|
28
|
+
from cbfpy.utils.general_utils import find_assets_dir, stdout_redirected
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DroneEnv(BaseEnv):
|
|
32
|
+
"""Drone Environment class
|
|
33
|
+
|
|
34
|
+
This provides an environment where the drone is contained to lie within a safe box,
|
|
35
|
+
and must avoid a movable obstacle.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
xyz_min (ArrayLike): Minimum bounds of the safe region, shape (3,)
|
|
39
|
+
xyz_max (ArrayLike): Maximum bounds of the safe region, shape (3,)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Constants
|
|
43
|
+
RADIUS = 0.1 # TODO tune
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
xyz_min: ArrayLike = (-0.5, -0.5, 0.5),
|
|
48
|
+
xyz_max: ArrayLike = (0.5, 0.5, 1.5),
|
|
49
|
+
):
|
|
50
|
+
# Suppress pybullet output + a Gym warning about float32 precision
|
|
51
|
+
with stdout_redirected(restore=False):
|
|
52
|
+
with warnings.catch_warnings():
|
|
53
|
+
warnings.simplefilter("ignore")
|
|
54
|
+
self._env = VelocityAviary(
|
|
55
|
+
# drone_model="cf2x",
|
|
56
|
+
num_drones=1,
|
|
57
|
+
initial_xyzs=np.array([[0, 0, 1]]),
|
|
58
|
+
initial_rpys=np.array([[0, 0, 0]]),
|
|
59
|
+
# physics="pyb",
|
|
60
|
+
neighbourhood_radius=np.inf,
|
|
61
|
+
pyb_freq=240,
|
|
62
|
+
ctrl_freq=48,
|
|
63
|
+
gui=True,
|
|
64
|
+
record=False,
|
|
65
|
+
obstacles=False,
|
|
66
|
+
user_debug_gui=False,
|
|
67
|
+
)
|
|
68
|
+
# Hack: Create same client interface as other envs
|
|
69
|
+
self.client: pybullet = object.__new__(BulletClient)
|
|
70
|
+
self.client._client = self._env.CLIENT
|
|
71
|
+
self.xyz_min = np.array(xyz_min)
|
|
72
|
+
self.xyz_max = np.array(xyz_max)
|
|
73
|
+
assert len(self.xyz_min) == len(self.xyz_max) == 3
|
|
74
|
+
self.client.setAdditionalSearchPath(find_assets_dir())
|
|
75
|
+
self.obstacle = self.client.loadURDF("point_robot.urdf", basePosition=(1, 1, 1))
|
|
76
|
+
self.client.configureDebugVisualizer(self.client.COV_ENABLE_GUI, 0)
|
|
77
|
+
self.client.resetDebugVisualizerCamera(1.80, 37.60, -25.00, (0.05, 0.03, 0.75))
|
|
78
|
+
self.robot = self._env.DRONE_IDS[0]
|
|
79
|
+
self.client.changeVisualShape(self.obstacle, -1, rgbaColor=[1, 0, 0, 1])
|
|
80
|
+
self.client.changeDynamics(self.obstacle, -1, angularDamping=10)
|
|
81
|
+
self.box = visualize_3D_box(
|
|
82
|
+
[self.xyz_min - self.RADIUS, self.xyz_max + self.RADIUS],
|
|
83
|
+
rgba=(0, 1, 0, 0.5),
|
|
84
|
+
)
|
|
85
|
+
# For color determination
|
|
86
|
+
self.is_in_box = True
|
|
87
|
+
self.tol = 1e-3
|
|
88
|
+
|
|
89
|
+
self.action = np.array([[0.0, 0.0, 0.0, 0.0]])
|
|
90
|
+
self.obs, self.reward, self.terminated, self.truncated, self.info = (
|
|
91
|
+
self._env.step(self.action)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _update_color(self, robot_pos: ArrayLike) -> None:
|
|
95
|
+
"""Update the color of the box depending on if the robot is inside or not (Green inside, red outside)"""
|
|
96
|
+
robot_pos = np.array(robot_pos)
|
|
97
|
+
if np.any(robot_pos < self.xyz_min - self.tol) or np.any(
|
|
98
|
+
robot_pos > self.xyz_max + self.tol
|
|
99
|
+
):
|
|
100
|
+
if self.is_in_box:
|
|
101
|
+
self.client.changeVisualShape(self.box, -1, rgbaColor=[1, 0, 0, 0.5])
|
|
102
|
+
self.is_in_box = False
|
|
103
|
+
else:
|
|
104
|
+
if not self.is_in_box:
|
|
105
|
+
self.client.changeVisualShape(self.box, -1, rgbaColor=[0, 1, 0, 0.5])
|
|
106
|
+
self.is_in_box = True
|
|
107
|
+
|
|
108
|
+
def get_state(self) -> Array:
|
|
109
|
+
robot_pos = self.client.getBasePositionAndOrientation(self.robot)[0]
|
|
110
|
+
robot_vel = self.client.getBaseVelocity(self.robot)[0]
|
|
111
|
+
self._update_color(robot_pos)
|
|
112
|
+
obstacle_pos = self.client.getBasePositionAndOrientation(self.obstacle)[0]
|
|
113
|
+
obstacle_vel = self.client.getBaseVelocity(self.obstacle)[0]
|
|
114
|
+
return np.array([*robot_pos, *robot_vel]), np.array(
|
|
115
|
+
[*obstacle_pos, *obstacle_vel]
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def get_desired_state(self) -> Array:
|
|
119
|
+
return np.array([0, 0, 1, 0, 0, 0])
|
|
120
|
+
|
|
121
|
+
def apply_control(self, u: Array) -> None:
|
|
122
|
+
# Note: the gym-pybullet-drones API has a weird format for the "velocity action" to take
|
|
123
|
+
self.action = np.array([[*u, np.linalg.norm(u)]])
|
|
124
|
+
|
|
125
|
+
def step(self):
|
|
126
|
+
# Step the gym-pybullet-drones environment using the last stored action
|
|
127
|
+
self.obs, self.reward, self.terminated, self.truncated, self.info = (
|
|
128
|
+
self._env.step(self.action)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _test_env():
|
|
133
|
+
"""Test the environment behavior under an **unsafe** nominal controller"""
|
|
134
|
+
|
|
135
|
+
env = DroneEnv()
|
|
136
|
+
|
|
137
|
+
def nominal_controller(z, z_des):
|
|
138
|
+
Kp = 1.0
|
|
139
|
+
Kv = 1.0
|
|
140
|
+
return Kp * (z_des[:3] - z[:3]) + Kv * (z_des[3:] - z[3:])
|
|
141
|
+
|
|
142
|
+
print("\nDemoing the drone environment with an unsafe controller")
|
|
143
|
+
while True:
|
|
144
|
+
z, z_obs = env.get_state()
|
|
145
|
+
z_des = env.get_desired_state()
|
|
146
|
+
u = nominal_controller(z, z_des)
|
|
147
|
+
env.apply_control(u)
|
|
148
|
+
env.step()
|
|
149
|
+
time.sleep(1 / 300)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
_test_env()
|