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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Simulation environments for point robots
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from jax import Array
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
from jax.typing import ArrayLike
|
|
9
|
+
import pybullet
|
|
10
|
+
from pybullet_utils.bullet_client import BulletClient
|
|
11
|
+
|
|
12
|
+
from cbfpy.utils.visualization import visualize_3D_box
|
|
13
|
+
from cbfpy.envs.base_env import BaseEnv
|
|
14
|
+
from cbfpy.utils.general_utils import find_assets_dir, stdout_redirected
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PointRobotEnv(BaseEnv):
|
|
18
|
+
"""Simulation environment for a point robot trying to approach a target position in 3D,
|
|
19
|
+
while remaining in a safe set defined as a box
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
xyz_min (ArrayLike, optional): Minimum bounds of the safe region, shape (3,). Defaults to (-1.0, -1.0, -1.0)
|
|
23
|
+
xyz_max (ArrayLike, optional): Maximum bounds of the safe region, shape (3,). Defaults to (1.0, 1.0, 1.0)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Constants
|
|
27
|
+
# Based on the values in the point robot URDF
|
|
28
|
+
URDF = "point_robot.urdf"
|
|
29
|
+
RADIUS = 0.25
|
|
30
|
+
MASS = 1.0
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
xyz_min: ArrayLike = (-1.0, -1.0, -1.0),
|
|
35
|
+
xyz_max: ArrayLike = (1.0, 1.0, 1.0),
|
|
36
|
+
):
|
|
37
|
+
self.xyz_min = np.array(xyz_min)
|
|
38
|
+
self.xyz_max = np.array(xyz_max)
|
|
39
|
+
assert len(self.xyz_min) == len(self.xyz_max) == 3
|
|
40
|
+
with stdout_redirected(restore=False):
|
|
41
|
+
self.client: pybullet = BulletClient(pybullet.GUI)
|
|
42
|
+
self.client.setAdditionalSearchPath(find_assets_dir())
|
|
43
|
+
self.client.configureDebugVisualizer(self.client.COV_ENABLE_GUI, 0)
|
|
44
|
+
self.robot = self.client.loadURDF(self.URDF)
|
|
45
|
+
self.target = self.client.loadURDF(self.URDF, basePosition=(3, 1, 1))
|
|
46
|
+
self.client.changeVisualShape(self.target, -1, rgbaColor=[1, 0, 0, 1])
|
|
47
|
+
self.client.changeDynamics(self.target, -1, linearDamping=10, angularDamping=10)
|
|
48
|
+
self.box = visualize_3D_box(
|
|
49
|
+
[self.xyz_min - self.RADIUS, self.xyz_max + self.RADIUS],
|
|
50
|
+
rgba=(0, 1, 0, 0.5),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# For color determination
|
|
54
|
+
self.is_in_box = True
|
|
55
|
+
self.tol = 1e-3
|
|
56
|
+
|
|
57
|
+
def _update_color(self, robot_pos: ArrayLike) -> None:
|
|
58
|
+
"""Update the color of the box depending on if the robot is inside or not (Green inside, red outside)"""
|
|
59
|
+
robot_pos = np.array(robot_pos)
|
|
60
|
+
if np.any(robot_pos < self.xyz_min - self.tol) or np.any(
|
|
61
|
+
robot_pos > self.xyz_max + self.tol
|
|
62
|
+
):
|
|
63
|
+
if self.is_in_box:
|
|
64
|
+
self.client.changeVisualShape(self.box, -1, rgbaColor=[1, 0, 0, 0.5])
|
|
65
|
+
self.is_in_box = False
|
|
66
|
+
else:
|
|
67
|
+
if not self.is_in_box:
|
|
68
|
+
self.client.changeVisualShape(self.box, -1, rgbaColor=[0, 1, 0, 0.5])
|
|
69
|
+
self.is_in_box = True
|
|
70
|
+
|
|
71
|
+
def get_state(self) -> Array:
|
|
72
|
+
robot_pos = self.client.getBasePositionAndOrientation(self.robot)[0]
|
|
73
|
+
robot_vel = self.client.getBaseVelocity(self.robot)[0]
|
|
74
|
+
self._update_color(robot_pos)
|
|
75
|
+
return np.array([*robot_pos, *robot_vel])
|
|
76
|
+
|
|
77
|
+
def get_desired_state(self) -> Array:
|
|
78
|
+
target_pos = self.client.getBasePositionAndOrientation(self.target)[0]
|
|
79
|
+
target_vel = self.client.getBaseVelocity(self.target)[0]
|
|
80
|
+
return np.array([*target_pos, *target_vel])
|
|
81
|
+
|
|
82
|
+
def apply_control(self, u: Array) -> None:
|
|
83
|
+
robot_pos = self.client.getBasePositionAndOrientation(self.robot)[0]
|
|
84
|
+
self.client.applyExternalForce(
|
|
85
|
+
self.robot, -1, u, robot_pos, self.client.WORLD_FRAME
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def step(self):
|
|
89
|
+
self.client.stepSimulation()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PointRobotObstacleEnv(BaseEnv):
|
|
93
|
+
"""Simulation environment for a point robot avoiding a movable obstacle,
|
|
94
|
+
while remaining in a safe set defined as a box
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
robot_pos (ArrayLike, optional): Initial position of the robot. Defaults to (0, 0, 0).
|
|
98
|
+
robot_vel (ArrayLike, optional): Initial velocity of the robot. Defaults to (0, 0, 0).
|
|
99
|
+
obstacle_pos (ArrayLike, optional): Initial position of the obstacle. Defaults to (1, 1, 1).
|
|
100
|
+
obstacle_vel (ArrayLike, optional): Initial velocity of the obstacle. Defaults to (-0.5, -0.6, -0.7).
|
|
101
|
+
xyz_min (ArrayLike, optional): Minimum bounds of the safe region, shape (3,). Defaults to (-1.0, -1.0, -1.0)
|
|
102
|
+
xyz_max (ArrayLike, optional): Maximum bounds of the safe region, shape (3,). Defaults to (1.0, 1.0, 1.0)
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
# Constants
|
|
106
|
+
# Based on the values in the point robot URDF
|
|
107
|
+
URDF = "point_robot.urdf"
|
|
108
|
+
RADIUS = 0.25
|
|
109
|
+
MASS = 1.0
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
robot_pos: ArrayLike = (0, 0, 0),
|
|
114
|
+
robot_vel: ArrayLike = (0, 0, 0),
|
|
115
|
+
obstacle_pos: ArrayLike = (1, 1, 1),
|
|
116
|
+
obstacle_vel: ArrayLike = (-0.5, -0.6, -0.7),
|
|
117
|
+
xyz_min: ArrayLike = (-1.0, -1.0, -1.0),
|
|
118
|
+
xyz_max: ArrayLike = (1.0, 1.0, 1.0),
|
|
119
|
+
):
|
|
120
|
+
with stdout_redirected(restore=False):
|
|
121
|
+
self.client: pybullet = BulletClient(pybullet.GUI)
|
|
122
|
+
self.client.setAdditionalSearchPath(find_assets_dir())
|
|
123
|
+
self.client.configureDebugVisualizer(self.client.COV_ENABLE_GUI, 0)
|
|
124
|
+
self.robot = self.client.loadURDF(self.URDF, basePosition=robot_pos)
|
|
125
|
+
self.target = self.client.loadURDF(self.URDF, basePosition=obstacle_pos)
|
|
126
|
+
self.client.changeVisualShape(self.target, -1, rgbaColor=[1, 0, 0, 1])
|
|
127
|
+
self.client.changeDynamics(self.target, -1, angularDamping=10)
|
|
128
|
+
self.client.resetBaseVelocity(self.target, obstacle_vel, (0, 0, 0))
|
|
129
|
+
self.client.resetBaseVelocity(self.robot, robot_vel, (0, 0, 0))
|
|
130
|
+
self.client.addUserDebugPoints([[0, 0, 0]], [[1, 0, 0]], 10, 0)
|
|
131
|
+
self.xyz_min = np.array(xyz_min)
|
|
132
|
+
self.xyz_max = np.array(xyz_max)
|
|
133
|
+
self.box = visualize_3D_box(
|
|
134
|
+
[self.xyz_min - self.RADIUS, self.xyz_max + self.RADIUS],
|
|
135
|
+
rgba=(0, 1, 0, 0.5),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def get_state(self):
|
|
139
|
+
robot_pos = self.client.getBasePositionAndOrientation(self.robot)[0]
|
|
140
|
+
robot_vel = self.client.getBaseVelocity(self.robot)[0]
|
|
141
|
+
obstacle_pos = self.client.getBasePositionAndOrientation(self.target)[0]
|
|
142
|
+
obstacle_vel = self.client.getBaseVelocity(self.target)[0]
|
|
143
|
+
return np.array([*robot_pos, *robot_vel]), np.array(
|
|
144
|
+
[*obstacle_pos, *obstacle_vel]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def get_desired_state(self):
|
|
148
|
+
return np.zeros(6)
|
|
149
|
+
|
|
150
|
+
def apply_control(self, u):
|
|
151
|
+
robot_pos = self.client.getBasePositionAndOrientation(self.robot)[0]
|
|
152
|
+
self.client.applyExternalForce(
|
|
153
|
+
self.robot, -1, u, robot_pos, self.client.WORLD_FRAME
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def step(self):
|
|
157
|
+
self.client.stepSimulation()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
## Tests ##
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _test_standard_env():
|
|
164
|
+
def nominal_controller(z, z_des):
|
|
165
|
+
# Use a PD controller to try to "touch" the target robot
|
|
166
|
+
Kp = 1.0
|
|
167
|
+
Kd = 1.0
|
|
168
|
+
pos_diff = z_des[:3] - z[:3]
|
|
169
|
+
des_pos = z_des[:3] - 2 * PointRobotEnv.RADIUS * pos_diff / jnp.linalg.norm(
|
|
170
|
+
pos_diff
|
|
171
|
+
)
|
|
172
|
+
return -Kp * (z[:3] - des_pos) - Kd * (z[3:] - z_des[3:])
|
|
173
|
+
|
|
174
|
+
env = PointRobotEnv()
|
|
175
|
+
while True:
|
|
176
|
+
z = env.get_state()
|
|
177
|
+
z_des = env.get_desired_state()
|
|
178
|
+
u = nominal_controller(z, z_des)
|
|
179
|
+
env.apply_control(u)
|
|
180
|
+
env.step()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _test_obstacle_env():
|
|
184
|
+
def nominal_controller(z, z_des):
|
|
185
|
+
Kp = 1.0
|
|
186
|
+
Kd = 1.0
|
|
187
|
+
return -Kp * (z[:3] - z_des[:3]) - Kd * (z[3:] - z_des[3:])
|
|
188
|
+
|
|
189
|
+
env = PointRobotObstacleEnv()
|
|
190
|
+
while True:
|
|
191
|
+
z, z_obs = env.get_state()
|
|
192
|
+
z_des = env.get_desired_state()
|
|
193
|
+
u = nominal_controller(z, z_des)
|
|
194
|
+
env.apply_control(u)
|
|
195
|
+
env.step()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _test_env(env_class: BaseEnv):
|
|
199
|
+
print("\nDemoing the point robot environment with an unsafe controller")
|
|
200
|
+
if env_class == PointRobotEnv:
|
|
201
|
+
return _test_standard_env()
|
|
202
|
+
elif env_class == PointRobotObstacleEnv:
|
|
203
|
+
return _test_obstacle_env()
|
|
204
|
+
else:
|
|
205
|
+
raise ValueError("Invalid env class")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
_test_env(PointRobotEnv)
|
|
File without changes
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Adaptive Cruise Control CLF-CBF Demo
|
|
3
|
+
|
|
4
|
+
This will enforce that the follower vehicle maintains a safe distance from the leader vehicle,
|
|
5
|
+
while also tracking a desired velocity.
|
|
6
|
+
|
|
7
|
+
We define the state z as [v_follower, v_leader, follow_distance] and the control u as the follower's wheel force
|
|
8
|
+
|
|
9
|
+
The dynamics incorporate a simple drag force model using empirically-derived coefficients
|
|
10
|
+
|
|
11
|
+
Note: There are a few parameters to tune in this CLF-CBF, such as the weightings between the inputs
|
|
12
|
+
the slack variable in the CLF objective. This is tricky to tune in general and values have been left
|
|
13
|
+
at what has been seen in other references.
|
|
14
|
+
|
|
15
|
+
Reference:
|
|
16
|
+
|
|
17
|
+
- "Control Barrier Function Based Quadratic Programs for Safety Critical Systems" - TAC 2017
|
|
18
|
+
- "Control Barrier Function based Quadratic Programs with Application to Adaptive Cruise Control" - CDC 2014
|
|
19
|
+
|
|
20
|
+
Some parameters are based on Jason Choi's https://github.com/HybridRobotics/CBF-CLF-Helper
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from jax import Array
|
|
24
|
+
import jax.numpy as jnp
|
|
25
|
+
from jax.typing import ArrayLike
|
|
26
|
+
|
|
27
|
+
from cbfpy.envs.car_env import VehicleEnv
|
|
28
|
+
from cbfpy.config.clf_cbf_config import CLFCBFConfig
|
|
29
|
+
from cbfpy.cbfs.clf_cbf import CLFCBF
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ACCConfig(CLFCBFConfig):
|
|
33
|
+
"""Configuration for the Adaptive Cruise Control CLF-CBF demo"""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self.gravity = 9.81
|
|
37
|
+
self.mass = 1650.0
|
|
38
|
+
self.drag_coeffs = (0.1, 5.0, 0.25) # Drag coeffs
|
|
39
|
+
self.v_des = 24.0 # Desired velocity
|
|
40
|
+
self.T = 1.8 # Lookahead time
|
|
41
|
+
self.cd = 0.3 # Coefficient of maximum deceleration
|
|
42
|
+
self.ca = 0.3 # Coefficient of maximum acceleration
|
|
43
|
+
u_min = -self.cd * self.mass * self.gravity # Min. control input (max braking)
|
|
44
|
+
u_max = self.ca * self.mass * self.gravity # Max. control input (max throttle)
|
|
45
|
+
super().__init__(
|
|
46
|
+
n=3,
|
|
47
|
+
m=1,
|
|
48
|
+
u_min=u_min,
|
|
49
|
+
u_max=u_max,
|
|
50
|
+
# Note: Relaxing the CLF-CBF QP is tricky because there is an additional relaxation
|
|
51
|
+
# parameter already, balancing the CLF and CBF constraints.
|
|
52
|
+
relax_cbf=False,
|
|
53
|
+
# If indeed relaxing, ensure that the QP relaxation >> the CLF relaxation
|
|
54
|
+
clf_relaxation_penalty=10.0,
|
|
55
|
+
cbf_relaxation_penalty=1e6,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def drag_force(self, v: float) -> float:
|
|
59
|
+
"""Compute the drag force on the follower car using a simple polynomial model
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
v (float): Velocity of the follower vehicle, in m/s
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
float: Drag force, in Newtons
|
|
66
|
+
"""
|
|
67
|
+
return (
|
|
68
|
+
self.drag_coeffs[0] + self.drag_coeffs[1] * v + self.drag_coeffs[2] * v**2
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def f(self, z: ArrayLike) -> Array:
|
|
72
|
+
v_f, v_l, D = z
|
|
73
|
+
# Note: We assume that the leader vehicle is at constant velocity here
|
|
74
|
+
return jnp.array([-self.drag_force(v_f) / self.mass, 0.0, v_l - v_f])
|
|
75
|
+
|
|
76
|
+
def g(self, z: ArrayLike) -> Array:
|
|
77
|
+
return jnp.array([(1 / self.mass), 0.0, 0.0]).reshape(-1, 1)
|
|
78
|
+
|
|
79
|
+
def h_1(self, z: ArrayLike) -> Array:
|
|
80
|
+
v_f, v_l, D = z
|
|
81
|
+
return jnp.array(
|
|
82
|
+
[D - self.T * v_f - 0.5 * (v_l - v_f) ** 2 / (self.cd * self.gravity)]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def V_1(self, z: ArrayLike) -> float:
|
|
86
|
+
# CLF: Squared error between the follower velocity and the desired velocity
|
|
87
|
+
return jnp.array([(z[0] - self.v_des) ** 2])
|
|
88
|
+
|
|
89
|
+
def H(self, z: ArrayLike) -> Array:
|
|
90
|
+
return jnp.eye(self.m) * (2 / self.mass**2)
|
|
91
|
+
|
|
92
|
+
def F(self, z: ArrayLike) -> Array:
|
|
93
|
+
return jnp.array([-2 * self.drag_force(z[0]) / self.mass**2])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main():
|
|
97
|
+
config = ACCConfig()
|
|
98
|
+
clf_cbf = CLFCBF.from_config(config)
|
|
99
|
+
# Ensure that parameters match up between the environment and the CLF-CBF
|
|
100
|
+
env = VehicleEnv(
|
|
101
|
+
"CLF-CBF Controller",
|
|
102
|
+
mass=config.mass,
|
|
103
|
+
drag_coeffs=config.drag_coeffs,
|
|
104
|
+
v_des=config.v_des,
|
|
105
|
+
u_min=config.u_min,
|
|
106
|
+
u_max=config.u_max,
|
|
107
|
+
)
|
|
108
|
+
while env.running:
|
|
109
|
+
z = env.get_state()
|
|
110
|
+
z_des = env.get_desired_state()
|
|
111
|
+
u = clf_cbf.controller(z, z_des)
|
|
112
|
+
env.apply_control(u)
|
|
113
|
+
env.step()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
main()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Drone Obstacle Avoidance Demo
|
|
3
|
+
|
|
4
|
+
We use velocity control, which can be easily applied to other drones
|
|
5
|
+
(for instance, PX4-controlled quadrotors can take in velocity commands)
|
|
6
|
+
|
|
7
|
+
The CBF uses a reduced model of the drone dynamics, while the simulation environment
|
|
8
|
+
does reflect a (somewhat-accurate) model of the true drone dynamics
|
|
9
|
+
|
|
10
|
+
Here, our state is the position and velocity of the drone: z = [position, velocity]
|
|
11
|
+
and the control input is the velocity of the drone: u = [velocity]
|
|
12
|
+
|
|
13
|
+
We also incorporate the state of the obstacle as an additional input to the CBF: z_obs = [position, velocity]
|
|
14
|
+
|
|
15
|
+
Note: since there is gravity in this simulation, the obstacle will fall to the ground initially. This is fine: just
|
|
16
|
+
use the mouse to drag it around near the drone to see the CBF response
|
|
17
|
+
|
|
18
|
+
See https://danielpmorton.github.io/drone_fencing/ for a demo of this on real hardware, and see the "point robot
|
|
19
|
+
obstacle avoidance" demo in CBFpy for a simplified version of this demo
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import time
|
|
23
|
+
import jax
|
|
24
|
+
import jax.numpy as jnp
|
|
25
|
+
|
|
26
|
+
from cbfpy import CBF, CBFConfig
|
|
27
|
+
from cbfpy.envs.drone_env import DroneEnv
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DroneConfig(CBFConfig):
|
|
31
|
+
"""Config for the drone obstacle avoidance / safe set containment demo"""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.mass = 1.0
|
|
35
|
+
self.pos_min = jnp.array([-2.0, -2.0, 0.7])
|
|
36
|
+
self.pos_max = jnp.array([2.0, 2.0, 2.0])
|
|
37
|
+
init_z_obs = jnp.array([3.0, 1.0, 1.0, -0.1, -0.1, -0.1])
|
|
38
|
+
super().__init__(
|
|
39
|
+
n=6, # State = [position, velocity]
|
|
40
|
+
m=3, # Control = [velocity]
|
|
41
|
+
relax_cbf=True,
|
|
42
|
+
init_args=(init_z_obs,),
|
|
43
|
+
cbf_relaxation_penalty=1e6,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def f(self, z):
|
|
47
|
+
# Assume we are directly controlling the robot's velocity
|
|
48
|
+
return jnp.zeros(self.n)
|
|
49
|
+
|
|
50
|
+
def g(self, z):
|
|
51
|
+
# Assume we are directly controlling the robot's velocity
|
|
52
|
+
return jnp.block([[jnp.eye(3)], [jnp.zeros((3, 3))]])
|
|
53
|
+
|
|
54
|
+
def h_1(self, z, z_obs):
|
|
55
|
+
obstacle_radius = 0.25
|
|
56
|
+
robot_radius = 0.25
|
|
57
|
+
padding = 0.1
|
|
58
|
+
pos_robot = z[:3]
|
|
59
|
+
vel_robot = z[3:]
|
|
60
|
+
pos_obs = z_obs[:3]
|
|
61
|
+
vel_obs = z_obs[3:]
|
|
62
|
+
dist_between_centers = jnp.linalg.norm(pos_obs - pos_robot)
|
|
63
|
+
dir_obs_to_robot = (pos_robot - pos_obs) / dist_between_centers
|
|
64
|
+
collision_velocity_component = (vel_obs - vel_robot).T @ dir_obs_to_robot
|
|
65
|
+
lookahead_time = 2.0
|
|
66
|
+
padding = 0.1
|
|
67
|
+
h_obstacle_avoidance = jnp.array(
|
|
68
|
+
[
|
|
69
|
+
dist_between_centers
|
|
70
|
+
- collision_velocity_component * lookahead_time
|
|
71
|
+
- obstacle_radius
|
|
72
|
+
- robot_radius
|
|
73
|
+
- padding
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
h_box_containment = jnp.concatenate([self.pos_max - z[:3], z[:3] - self.pos_min])
|
|
77
|
+
return jnp.concatenate([h_obstacle_avoidance, h_box_containment])
|
|
78
|
+
|
|
79
|
+
def alpha(self, h):
|
|
80
|
+
return jnp.array([3.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) * h
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def nominal_controller(z, z_des):
|
|
84
|
+
Kp = 1.0
|
|
85
|
+
Kv = 1.0
|
|
86
|
+
return Kp * (z_des[:3] - z[:3]) + Kv * (z_des[3:] - z[3:])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main():
|
|
90
|
+
config = DroneConfig()
|
|
91
|
+
cbf = CBF.from_config(config)
|
|
92
|
+
env = DroneEnv(config.pos_min, config.pos_max)
|
|
93
|
+
|
|
94
|
+
@jax.jit
|
|
95
|
+
def safe_controller(z, z_des, z_obs):
|
|
96
|
+
u = nominal_controller(z, z_des)
|
|
97
|
+
return cbf.safety_filter(z, u, z_obs)
|
|
98
|
+
|
|
99
|
+
while True:
|
|
100
|
+
z, z_obs = env.get_state()
|
|
101
|
+
z_des = env.get_desired_state()
|
|
102
|
+
u = safe_controller(z, z_des, z_obs)
|
|
103
|
+
env.apply_control(u)
|
|
104
|
+
env.step()
|
|
105
|
+
time.sleep(1 / 300)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Manipulator Joint Limit Avoidance Demo
|
|
3
|
+
|
|
4
|
+
We will command a joint position trajectory that exceeds the joint limits, and the CBF will ensure that we stay
|
|
5
|
+
within the limits (+ some margin)
|
|
6
|
+
|
|
7
|
+
This uses a single-integrator reduced model of the manipulator dynamics.
|
|
8
|
+
We define the state as the joint positions and assume that we can directly control the joint velocities
|
|
9
|
+
i.e. z = [q1, q2, q3] and u = [q1_dot, q2_dot, q3_dot]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import jax
|
|
14
|
+
import jax.numpy as jnp
|
|
15
|
+
from jax import Array
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
from matplotlib.lines import Line2D
|
|
18
|
+
|
|
19
|
+
from cbfpy import CBF, CBFConfig
|
|
20
|
+
from cbfpy.envs.arm_envs import JointLimitsEnv
|
|
21
|
+
from cbfpy.utils.general_utils import find_assets_dir
|
|
22
|
+
|
|
23
|
+
URDF = find_assets_dir() + "three_dof_arm.urdf"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class JointLimitsConfig(CBFConfig):
|
|
27
|
+
"""Config for the 3-DOF arm, avoiding its joint limits using velocity control"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.num_joints = 3
|
|
31
|
+
# Joint limit values from the URDF
|
|
32
|
+
self.q_min = -np.pi / 2 * np.ones(self.num_joints)
|
|
33
|
+
self.q_max = np.pi / 2 * np.ones(self.num_joints)
|
|
34
|
+
# Pad joint limts (to better evauate CBF performance)
|
|
35
|
+
self.padding = 0.3
|
|
36
|
+
super().__init__(n=self.num_joints, m=self.num_joints)
|
|
37
|
+
|
|
38
|
+
def f(self, z):
|
|
39
|
+
return jnp.zeros(self.num_joints)
|
|
40
|
+
|
|
41
|
+
def g(self, z):
|
|
42
|
+
return jnp.eye(self.num_joints)
|
|
43
|
+
|
|
44
|
+
def h_1(self, z):
|
|
45
|
+
q = z
|
|
46
|
+
return jnp.concatenate(
|
|
47
|
+
[self.q_max - q - self.padding, q - self.q_min - self.padding]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def nominal_controller(q: Array, q_des: Array) -> Array:
|
|
52
|
+
"""Very simple proportional controller: Commands joint velocities to reduce a position error
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
q (Array): Joint positions, shape (num_joints,)
|
|
56
|
+
q_des (Array): Desired joint positions, shape (num_joints,)
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Array: Joint velocity command, shape (num_joints,)
|
|
60
|
+
"""
|
|
61
|
+
k = 1.0
|
|
62
|
+
return k * (q_des - q)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
config = JointLimitsConfig()
|
|
67
|
+
cbf = CBF.from_config(config)
|
|
68
|
+
env = JointLimitsEnv()
|
|
69
|
+
|
|
70
|
+
@jax.jit
|
|
71
|
+
def safety_filter(q, u):
|
|
72
|
+
return cbf.safety_filter(q, u)
|
|
73
|
+
|
|
74
|
+
q_hist = []
|
|
75
|
+
q_des_hist = []
|
|
76
|
+
u_safe_hist = []
|
|
77
|
+
u_unsafe_hist = []
|
|
78
|
+
sim_time = 100.0
|
|
79
|
+
timestep = env.timestep
|
|
80
|
+
num_timesteps = int(sim_time / timestep)
|
|
81
|
+
for i in range(num_timesteps):
|
|
82
|
+
q = env.get_state()
|
|
83
|
+
q_des = env.get_desired_state()
|
|
84
|
+
u_unsafe = nominal_controller(q, q_des)
|
|
85
|
+
u = safety_filter(q, u_unsafe)
|
|
86
|
+
env.apply_control(u)
|
|
87
|
+
env.step()
|
|
88
|
+
q_hist.append(q)
|
|
89
|
+
q_des_hist.append(q_des)
|
|
90
|
+
u_unsafe_hist.append(u_unsafe)
|
|
91
|
+
u_safe_hist.append(u)
|
|
92
|
+
|
|
93
|
+
## Plotting ##
|
|
94
|
+
|
|
95
|
+
fig, axs = plt.subplots(2, 3)
|
|
96
|
+
ts = timestep * np.arange(num_timesteps)
|
|
97
|
+
|
|
98
|
+
# On the top row, plot the q and q des for each joint, along with the joint limits indicated
|
|
99
|
+
for i in range(3):
|
|
100
|
+
(q_line,) = axs[0, i].plot(ts, np.array(q_hist)[:, i], label="q")
|
|
101
|
+
(q_des_line,) = axs[0, i].plot(ts, np.array(q_des_hist)[:, i], label="q_des")
|
|
102
|
+
axs[0, i].plot(
|
|
103
|
+
ts,
|
|
104
|
+
env.q_min[i] * np.ones(num_timesteps),
|
|
105
|
+
ls="--",
|
|
106
|
+
c="red",
|
|
107
|
+
label="q_min (True)",
|
|
108
|
+
)
|
|
109
|
+
axs[0, i].plot(
|
|
110
|
+
ts,
|
|
111
|
+
env.q_max[i] * np.ones(num_timesteps),
|
|
112
|
+
ls="--",
|
|
113
|
+
c="red",
|
|
114
|
+
label="q_max (True)",
|
|
115
|
+
)
|
|
116
|
+
axs[0, i].plot(
|
|
117
|
+
ts,
|
|
118
|
+
(env.q_min[i] + config.padding) * np.ones(num_timesteps),
|
|
119
|
+
ls="--",
|
|
120
|
+
c="blue",
|
|
121
|
+
label="q_min (CBF)",
|
|
122
|
+
)
|
|
123
|
+
axs[0, i].plot(
|
|
124
|
+
ts,
|
|
125
|
+
(env.q_max[i] - config.padding) * np.ones(num_timesteps),
|
|
126
|
+
ls="--",
|
|
127
|
+
c="blue",
|
|
128
|
+
label="q_max (CBF)",
|
|
129
|
+
)
|
|
130
|
+
legend_elements = [
|
|
131
|
+
q_line,
|
|
132
|
+
q_des_line,
|
|
133
|
+
Line2D([0], [0], color="red", ls="--", label="True limits"),
|
|
134
|
+
Line2D([0], [0], color="blue", ls="--", label="CBF limits"),
|
|
135
|
+
]
|
|
136
|
+
axs[0, i].legend(handles=legend_elements, loc="lower left")
|
|
137
|
+
axs[0, i].set_title(f"Joint {i} position")
|
|
138
|
+
|
|
139
|
+
# On the bottom row, plot the safe and unsafe velocities for each joint
|
|
140
|
+
for i in range(3):
|
|
141
|
+
axs[1, i].plot(ts, np.array(u_safe_hist)[:, i], label="Safe")
|
|
142
|
+
axs[1, i].plot(ts, np.array(u_unsafe_hist)[:, i], label="Unsafe")
|
|
143
|
+
axs[1, i].legend(loc="lower left")
|
|
144
|
+
axs[1, i].set_title(f"Joint {i} velocity")
|
|
145
|
+
|
|
146
|
+
plt.show()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
main()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# Point Robot Safe-Set Containment Demo
|
|
3
|
+
|
|
4
|
+
Demo of a point robot in 3D constrained to lie within a box via a CBF safety filter on a PD controller
|
|
5
|
+
|
|
6
|
+
We define the state z as [x, y, z, vx, vy, vz] and the control u as [Fx, Fy, Fz]
|
|
7
|
+
|
|
8
|
+
The dynamics are that of a simple double integrator:
|
|
9
|
+
z_dot = [vx, vy, vz, 0, 0, 0] + [0, 0, 0, Fx/m, Fy/m, Fz/m]
|
|
10
|
+
|
|
11
|
+
In matrix form, this can be expressed as z_dot = A z + B u with A and B as implemented in the config.
|
|
12
|
+
|
|
13
|
+
The safety constraints are set as an upper and lower bound on the position of the robot.
|
|
14
|
+
|
|
15
|
+
This is a relative-degree-2 system, so we use the RD2 version of the CBF constraints.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import jax
|
|
19
|
+
from jax import Array
|
|
20
|
+
import jax.numpy as jnp
|
|
21
|
+
from jax.typing import ArrayLike
|
|
22
|
+
|
|
23
|
+
from cbfpy import CBF, CBFConfig
|
|
24
|
+
from cbfpy.envs.point_robot_envs import PointRobotEnv
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PointRobotConfig(CBFConfig):
|
|
28
|
+
"""Configuration for the 3D 'point-robot-in-a-box' example."""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.mass = 1.0
|
|
32
|
+
self.pos_min = jnp.array([-1.0, -1.0, -1.0])
|
|
33
|
+
self.pos_max = jnp.array([1.0, 1.0, 1.0])
|
|
34
|
+
super().__init__(n=6, m=3)
|
|
35
|
+
|
|
36
|
+
def f(self, z):
|
|
37
|
+
A = jnp.block(
|
|
38
|
+
[[jnp.zeros((3, 3)), jnp.eye(3)], [jnp.zeros((3, 3)), jnp.zeros((3, 3))]]
|
|
39
|
+
)
|
|
40
|
+
return A @ z
|
|
41
|
+
|
|
42
|
+
def g(self, z):
|
|
43
|
+
B = jnp.block([[jnp.zeros((3, 3))], [jnp.eye(3) / self.mass]])
|
|
44
|
+
return B
|
|
45
|
+
|
|
46
|
+
def h_2(self, z):
|
|
47
|
+
return jnp.concatenate([self.pos_max - z[:3], z[:3] - self.pos_min])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@jax.jit
|
|
51
|
+
def nominal_controller(z: ArrayLike, z_des: ArrayLike) -> Array:
|
|
52
|
+
"""A simple PD controller for the point robot.
|
|
53
|
+
|
|
54
|
+
This is unsafe without the CBF, as there is no guarantee that the robot will stay within the safe region
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
z (ArrayLike): The current state of the robot [x, y, z, vx, vy, vz]
|
|
58
|
+
z_des (ArrayLike): The desired state of the robot [x_des, y_des, z_des, vx_des, vy_des, vz_des]
|
|
59
|
+
"""
|
|
60
|
+
Kp = 1.0
|
|
61
|
+
Kd = 1.0
|
|
62
|
+
radius = 0.25
|
|
63
|
+
# We assume we are in our Pybullet simulation environment where we have two point robots loaded
|
|
64
|
+
# One is the robot we are controlling, and the other is the target robot, which is interactive via the GUI
|
|
65
|
+
# This will attempt to "touch" the target robot
|
|
66
|
+
pos_diff = z_des[:3] - z[:3]
|
|
67
|
+
des_pos = z_des[:3] - 2 * radius * pos_diff / jnp.linalg.norm(pos_diff)
|
|
68
|
+
u = -Kp * (z[:3] - des_pos) - Kd * (z[3:] - z_des[3:])
|
|
69
|
+
return u
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main():
|
|
73
|
+
config = PointRobotConfig()
|
|
74
|
+
cbf = CBF.from_config(config)
|
|
75
|
+
env = PointRobotEnv(config.pos_min, config.pos_max)
|
|
76
|
+
|
|
77
|
+
@jax.jit
|
|
78
|
+
def safe_controller(z, z_des):
|
|
79
|
+
u = nominal_controller(z, z_des)
|
|
80
|
+
return cbf.safety_filter(z, u)
|
|
81
|
+
|
|
82
|
+
while True:
|
|
83
|
+
z = env.get_state()
|
|
84
|
+
z_des = env.get_desired_state()
|
|
85
|
+
u = safe_controller(z, z_des)
|
|
86
|
+
env.apply_control(u)
|
|
87
|
+
env.step()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
main()
|