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