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/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()
@@ -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()