cbfpy 0.0.1__py3-none-any.whl → 0.0.4__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/cbfs/cbf.py +46 -70
- cbfpy/cbfs/clf_cbf.py +81 -86
- cbfpy/config/cbf_config.py +106 -56
- cbfpy/config/clf_cbf_config.py +79 -29
- cbfpy/envs/base_env.py +3 -3
- cbfpy/envs/drone_env.py +1 -1
- cbfpy/examples/adaptive_cruise_control_demo.py +5 -4
- cbfpy/examples/drone_demo.py +8 -6
- cbfpy/examples/joint_limits_demo.py +1 -1
- cbfpy/examples/point_robot_obstacle_demo.py +7 -7
- {cbfpy-0.0.1.dist-info → cbfpy-0.0.4.dist-info}/METADATA +24 -17
- cbfpy-0.0.4.dist-info/RECORD +32 -0
- {cbfpy-0.0.1.dist-info → cbfpy-0.0.4.dist-info}/WHEEL +1 -1
- test/test_speed.py +2 -2
- cbfpy/temp/test_import.py +0 -3
- cbfpy-0.0.1.dist-info/RECORD +0 -33
- {cbfpy-0.0.1.dist-info → cbfpy-0.0.4.dist-info/licenses}/LICENSE +0 -0
- {cbfpy-0.0.1.dist-info → cbfpy-0.0.4.dist-info}/top_level.txt +0 -0
cbfpy/config/cbf_config.py
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
## Defining the problem:
|
|
5
5
|
|
|
6
|
-
CBFs have two primary implementation requirements: the dynamics functions, and the barrier function(s).
|
|
6
|
+
CBFs have two primary implementation requirements: the dynamics functions, and the barrier function(s).
|
|
7
7
|
These can be specified through the `f`, `g`, and `h` methods, respectively. Note that the main requirements
|
|
8
8
|
for these functions are that (1) the dynamics are control-affine, and (2) the barrier function(s) are "zeroing"
|
|
9
9
|
barriers, as opposed to "reciprocal" barriers. A zeroing barrier is one which is positive in the interior of the
|
|
10
|
-
safe set, and zero on the boundary.
|
|
10
|
+
safe set, and zero on the boundary.
|
|
11
11
|
|
|
12
|
-
Depending on the relative degree of your barrier function(s), you should implement the `h_1` method
|
|
12
|
+
Depending on the relative degree of your barrier function(s), you should implement the `h_1` method
|
|
13
13
|
(for a relative-degree-1 barrier), and/or the `h_2` method (for a relative-degree-2 barrier).
|
|
14
14
|
|
|
15
15
|
## Tuning the CBF:
|
|
@@ -30,7 +30,7 @@ does require that P is positive semi-definite.
|
|
|
30
30
|
Depending on the construction of the barrier functions and if control limits are provided, the CBF QP may not always be
|
|
31
31
|
feasible. If allowing for relaxation in the CBFConfig, a slack variable will be introduced to ensure that the
|
|
32
32
|
problem is always feasible, with a high penalty on any infeasibility. This is generally useful for controller
|
|
33
|
-
robustness, but means that safety is not guaranteed.
|
|
33
|
+
robustness, but means that safety is not guaranteed.
|
|
34
34
|
|
|
35
35
|
If strict enforcement of the CBF is desired, your higest-level controller should handle the case where the QP
|
|
36
36
|
is infeasible.
|
|
@@ -67,13 +67,17 @@ class CBFConfig(ABC):
|
|
|
67
67
|
m (int): Control dimension
|
|
68
68
|
u_min (ArrayLike, optional): Minimum control input, shape (m,). Defaults to None (Unconstrained).
|
|
69
69
|
u_max (ArrayLike, optional): Maximum control input, shape (m,). Defaults to None (Unconstrained).
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
relax_qp (bool, optional): Whether to allow for relaxation in the CBF QP. Defaults to True.
|
|
71
|
+
Note: this is required for differentiability through the QP.
|
|
72
|
+
cbf_relaxation_penalty (float, optional): Penalty on the CBF slack variables in the relaxed QP.
|
|
73
|
+
Defaults to 1e3. Note: only applies if relax_qp is True.
|
|
74
|
+
control_relaxation_penalty (float, optional): Penalty on the control constraint slack variables in the
|
|
75
|
+
relaxed QP. Defaults to 1e5. Note: only applies if relax_qp is True.
|
|
73
76
|
solver_tol (float, optional): Tolerance for the QP solver. Defaults to 1e-3.
|
|
74
|
-
init_args (tuple, optional): If your
|
|
75
|
-
include an initial seed for these
|
|
76
|
-
|
|
77
|
+
init_args (tuple, optional): If your barriers or dynamics rely on additional (non-differentiable, static shape)
|
|
78
|
+
args other than just the state, include an initial seed for these args here. Defaults to None.
|
|
79
|
+
init_kwargs (dict, optional): If your barriers or dynamics rely on additional (non-differentiable, static shape)
|
|
80
|
+
kwargs other than just the state, include an initial seed for these kwargs here. Defaults to None.
|
|
77
81
|
"""
|
|
78
82
|
|
|
79
83
|
def __init__(
|
|
@@ -82,10 +86,12 @@ class CBFConfig(ABC):
|
|
|
82
86
|
m: int,
|
|
83
87
|
u_min: Optional[ArrayLike] = None,
|
|
84
88
|
u_max: Optional[ArrayLike] = None,
|
|
85
|
-
|
|
89
|
+
relax_qp: bool = True,
|
|
86
90
|
cbf_relaxation_penalty: float = 1e3,
|
|
91
|
+
control_relaxation_penalty: float = 1e5,
|
|
87
92
|
solver_tol: float = 1e-3,
|
|
88
|
-
init_args: tuple =
|
|
93
|
+
init_args: Optional[tuple] = None,
|
|
94
|
+
init_kwargs: Optional[dict] = None,
|
|
89
95
|
):
|
|
90
96
|
if not (isinstance(n, int) and n > 0):
|
|
91
97
|
raise ValueError(f"n must be a positive integer. Got: {n}")
|
|
@@ -95,9 +101,9 @@ class CBFConfig(ABC):
|
|
|
95
101
|
raise ValueError(f"m must be a positive integer. Got: {m}")
|
|
96
102
|
self.m = m
|
|
97
103
|
|
|
98
|
-
if not isinstance(
|
|
99
|
-
raise ValueError(f"
|
|
100
|
-
self.
|
|
104
|
+
if not isinstance(relax_qp, bool):
|
|
105
|
+
raise ValueError(f"relax_qp must be a boolean. Got: {relax_qp}")
|
|
106
|
+
self.relax_qp = relax_qp
|
|
101
107
|
|
|
102
108
|
if not (
|
|
103
109
|
isinstance(cbf_relaxation_penalty, (int, float))
|
|
@@ -112,10 +118,33 @@ class CBFConfig(ABC):
|
|
|
112
118
|
raise ValueError(f"solver_tol must be a positive value. Got: {solver_tol}")
|
|
113
119
|
self.solver_tol = float(solver_tol)
|
|
114
120
|
|
|
115
|
-
if
|
|
121
|
+
if self.solver_tol > 1e-2:
|
|
122
|
+
print(
|
|
123
|
+
f"WARNING: solver tolerance is quite high ({self.solver_tol}). "
|
|
124
|
+
+ " Solution will likely be poor."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if init_args is None:
|
|
128
|
+
init_args = ()
|
|
129
|
+
elif not isinstance(init_args, tuple):
|
|
116
130
|
raise ValueError(f"init_args must be a tuple. Got: {init_args}")
|
|
117
131
|
self.init_args = init_args
|
|
118
132
|
|
|
133
|
+
if init_kwargs is None:
|
|
134
|
+
init_kwargs = {}
|
|
135
|
+
elif not isinstance(init_kwargs, dict):
|
|
136
|
+
raise ValueError(f"init_kwargs must be a dict. Got: {init_kwargs}")
|
|
137
|
+
self.init_kwargs = init_kwargs
|
|
138
|
+
|
|
139
|
+
if not (
|
|
140
|
+
isinstance(control_relaxation_penalty, (int, float))
|
|
141
|
+
and control_relaxation_penalty >= 0
|
|
142
|
+
):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"control_relaxation_penalty must be a non-negative value. Got: {control_relaxation_penalty}"
|
|
145
|
+
)
|
|
146
|
+
self.control_relaxation_penalty = float(control_relaxation_penalty)
|
|
147
|
+
|
|
119
148
|
# Control limits require a bit of extra handling. They can be both None if unconstrained,
|
|
120
149
|
# but we should not have one limit as None and the other as some value
|
|
121
150
|
u_min = np.asarray(u_min, dtype=float).flatten() if u_min is not None else None
|
|
@@ -137,11 +166,17 @@ class CBFConfig(ABC):
|
|
|
137
166
|
self.u_min = u_min
|
|
138
167
|
self.u_max = u_max
|
|
139
168
|
|
|
169
|
+
if (
|
|
170
|
+
self.control_constrained
|
|
171
|
+
and self.control_relaxation_penalty < self.cbf_relaxation_penalty
|
|
172
|
+
):
|
|
173
|
+
print("WARNING: Control constraints have a lower penalty than the CBFs.")
|
|
174
|
+
|
|
140
175
|
# Test if the methods are provided and verify their output dimension
|
|
141
|
-
z_test =
|
|
142
|
-
u_test =
|
|
143
|
-
f_test = self.f(z_test)
|
|
144
|
-
g_test = self.g(z_test)
|
|
176
|
+
z_test = np.ones(self.n)
|
|
177
|
+
u_test = np.ones(self.m)
|
|
178
|
+
f_test = self.f(z_test, *self.init_args, **self.init_kwargs)
|
|
179
|
+
g_test = self.g(z_test, *self.init_args, **self.init_kwargs)
|
|
145
180
|
if f_test.shape != (self.n,):
|
|
146
181
|
raise ValueError(
|
|
147
182
|
f"Invalid shape for f(z). Got {f_test.shape}, expected ({self.n},)"
|
|
@@ -151,13 +186,10 @@ class CBFConfig(ABC):
|
|
|
151
186
|
f"Invalid shape for g(z). Got {g_test.shape}, expected ({self.n}, {self.m})"
|
|
152
187
|
)
|
|
153
188
|
try:
|
|
154
|
-
h1_test = self.h_1(z_test, *self.init_args)
|
|
155
|
-
h2_test = self.h_2(z_test, *self.init_args)
|
|
189
|
+
h1_test = self.h_1(z_test, *self.init_args, **self.init_kwargs)
|
|
190
|
+
h2_test = self.h_2(z_test, *self.init_args, **self.init_kwargs)
|
|
156
191
|
except TypeError as e:
|
|
157
|
-
raise ValueError(
|
|
158
|
-
"Cannot test the barrier function; likely missing additional arguments.\n"
|
|
159
|
-
+ "Please provide an initial seed for these args in the config's init_args input"
|
|
160
|
-
) from e
|
|
192
|
+
raise ValueError("Cannot test the barrier function") from e
|
|
161
193
|
if h1_test.ndim != 1 or h2_test.ndim != 1:
|
|
162
194
|
raise ValueError("Barrier function(s) must be 1D arrays")
|
|
163
195
|
self.num_rd1_cbf = h1_test.shape[0]
|
|
@@ -168,9 +200,9 @@ class CBFConfig(ABC):
|
|
|
168
200
|
"No barrier functions provided."
|
|
169
201
|
+ "\nYou can implement this via the h_1 and/or h_2 methods in your config class"
|
|
170
202
|
)
|
|
171
|
-
h_test =
|
|
172
|
-
alpha_test = self.alpha(h_test)
|
|
173
|
-
alpha_2_test = self.alpha_2(h2_test)
|
|
203
|
+
h_test = np.concatenate([h1_test, h2_test])
|
|
204
|
+
alpha_test = self.alpha(h_test, *self.init_args, **self.init_kwargs)
|
|
205
|
+
alpha_2_test = self.alpha_2(h2_test, *self.init_args, **self.init_kwargs)
|
|
174
206
|
if alpha_test.shape != (self.num_cbf,):
|
|
175
207
|
raise ValueError(
|
|
176
208
|
f"Invalid shape for alpha(h(z)): {alpha_test.shape}. Expected ({self.num_cbf},)"
|
|
@@ -181,15 +213,16 @@ class CBFConfig(ABC):
|
|
|
181
213
|
f"Invalid shape for alpha_2(h_2(z)): {alpha_2_test.shape}. Expected ({self.num_rd2_cbf},)"
|
|
182
214
|
+ "\nCheck that the output of the alpha_2() function matches the number of RD2 CBFs"
|
|
183
215
|
)
|
|
184
|
-
self._check_class_kappa(
|
|
185
|
-
|
|
216
|
+
self._check_class_kappa(
|
|
217
|
+
self.alpha, self.num_cbf, *self.init_args, **self.init_kwargs
|
|
218
|
+
)
|
|
219
|
+
self._check_class_kappa(
|
|
220
|
+
self.alpha_2, self.num_rd2_cbf, *self.init_args, **self.init_kwargs
|
|
221
|
+
)
|
|
186
222
|
try:
|
|
187
|
-
P_test = self.P(z_test, u_test, *self.init_args)
|
|
223
|
+
P_test = self.P(z_test, u_test, *self.init_args, **self.init_kwargs)
|
|
188
224
|
except TypeError as e:
|
|
189
|
-
raise ValueError(
|
|
190
|
-
"Cannot test the P matrix; likely missing additional arguments.\n"
|
|
191
|
-
+ "Please provide an initial seed for these args in the config's init_args input"
|
|
192
|
-
) from e
|
|
225
|
+
raise ValueError("Cannot test the P matrix") from e
|
|
193
226
|
if P_test.shape != (self.m, self.m):
|
|
194
227
|
raise ValueError(
|
|
195
228
|
f"Invalid shape for P(z). Got {P_test.shape}, expected ({self.m}, {self.m})"
|
|
@@ -197,10 +230,29 @@ class CBFConfig(ABC):
|
|
|
197
230
|
if not self._is_symmetric_psd(P_test):
|
|
198
231
|
raise ValueError("P matrix must be symmetric positive semi-definite")
|
|
199
232
|
|
|
233
|
+
# Handle QP relaxation penalties, if relaxation is enabled
|
|
234
|
+
num_qp_constraints = (
|
|
235
|
+
self.num_cbf if not self.control_constrained else self.num_cbf + 2 * self.m
|
|
236
|
+
)
|
|
237
|
+
if self.control_constrained:
|
|
238
|
+
self.constraint_relaxation_penalties = tuple(
|
|
239
|
+
np.concatenate(
|
|
240
|
+
[
|
|
241
|
+
self.cbf_relaxation_penalty * np.ones(self.num_cbf),
|
|
242
|
+
self.control_relaxation_penalty * np.ones(2 * self.m),
|
|
243
|
+
]
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
self.constraint_relaxation_penalties = tuple(
|
|
248
|
+
self.cbf_relaxation_penalty * np.ones(self.num_cbf)
|
|
249
|
+
)
|
|
250
|
+
assert len(self.constraint_relaxation_penalties) == num_qp_constraints
|
|
251
|
+
|
|
200
252
|
## Control Affine Dynamics ##
|
|
201
253
|
|
|
202
254
|
@abstractmethod
|
|
203
|
-
def f(self, z: ArrayLike) -> Array:
|
|
255
|
+
def f(self, z: ArrayLike, *args, **kwargs) -> Array:
|
|
204
256
|
"""The uncontrolled dynamics function. Possibly nonlinear, and locally Lipschitz
|
|
205
257
|
|
|
206
258
|
i.e. the function f, such that z_dot = f(z) + g(z) u
|
|
@@ -214,7 +266,7 @@ class CBFConfig(ABC):
|
|
|
214
266
|
pass
|
|
215
267
|
|
|
216
268
|
@abstractmethod
|
|
217
|
-
def g(self, z: ArrayLike) -> Array:
|
|
269
|
+
def g(self, z: ArrayLike, *args, **kwargs) -> Array:
|
|
218
270
|
"""The control affine dynamics function. Locally Lipschitz.
|
|
219
271
|
|
|
220
272
|
i.e. the function g, such that z_dot = f(z) + g(z) u
|
|
@@ -229,7 +281,7 @@ class CBFConfig(ABC):
|
|
|
229
281
|
|
|
230
282
|
## Barriers ##
|
|
231
283
|
|
|
232
|
-
def h_1(self, z: ArrayLike, *
|
|
284
|
+
def h_1(self, z: ArrayLike, *args, **kwargs) -> Array:
|
|
233
285
|
"""Relative-degree-1 barrier function(s).
|
|
234
286
|
|
|
235
287
|
A (zeroing) CBF is a continuously-differentiable function h, such that for any state z in the interior of
|
|
@@ -244,15 +296,13 @@ class CBFConfig(ABC):
|
|
|
244
296
|
|
|
245
297
|
Args:
|
|
246
298
|
z (ArrayLike): State, shape (n,)
|
|
247
|
-
*h_args: Optional additional arguments for the barrier function. Note: If using additional args with your
|
|
248
|
-
barrier, these must be a static shape/type, or else this will trigger a recompilation in Jax.
|
|
249
299
|
|
|
250
300
|
Returns:
|
|
251
301
|
Array: Barrier function(s), shape (num_rd1_barr,)
|
|
252
302
|
"""
|
|
253
303
|
return jnp.array([])
|
|
254
304
|
|
|
255
|
-
def h_2(self, z: ArrayLike, *
|
|
305
|
+
def h_2(self, z: ArrayLike, *args, **kwargs) -> Array:
|
|
256
306
|
"""Relative-degree-2 (high-order) barrier function(s).
|
|
257
307
|
|
|
258
308
|
A (zeroing) CBF is a continuously-differentiable function h, such that for any state z in the interior of
|
|
@@ -268,8 +318,6 @@ class CBFConfig(ABC):
|
|
|
268
318
|
|
|
269
319
|
Args:
|
|
270
320
|
z (ArrayLike): State, shape (n,)
|
|
271
|
-
*h_args: Optional additional arguments for the barrier function. Note: If using additional args with your
|
|
272
|
-
barrier, these must be a static shape/type, or else this will trigger a recompilation in Jax.
|
|
273
321
|
|
|
274
322
|
Returns:
|
|
275
323
|
Array: Barrier function(s), shape (num_rd2_barr,)
|
|
@@ -278,7 +326,7 @@ class CBFConfig(ABC):
|
|
|
278
326
|
|
|
279
327
|
## Additional tuning functions ##
|
|
280
328
|
|
|
281
|
-
def alpha(self, h: ArrayLike) -> Array:
|
|
329
|
+
def alpha(self, h: ArrayLike, *args, **kwargs) -> Array:
|
|
282
330
|
"""A class Kappa function, dictating the "gain" of the barrier function(s)
|
|
283
331
|
|
|
284
332
|
For reference, a class Kappa function is a monotonically increasing function which passes through the origin.
|
|
@@ -294,7 +342,7 @@ class CBFConfig(ABC):
|
|
|
294
342
|
"""
|
|
295
343
|
return h
|
|
296
344
|
|
|
297
|
-
def alpha_2(self, h_2: ArrayLike) -> Array:
|
|
345
|
+
def alpha_2(self, h_2: ArrayLike, *args, **kwargs) -> Array:
|
|
298
346
|
"""A second class Kappa function which dictactes the "gain" associated with the relative-degree-2
|
|
299
347
|
barrier functions
|
|
300
348
|
|
|
@@ -313,7 +361,7 @@ class CBFConfig(ABC):
|
|
|
313
361
|
|
|
314
362
|
# Objective function tuning
|
|
315
363
|
|
|
316
|
-
def P(self, z: Array, u_des: Array, *
|
|
364
|
+
def P(self, z: Array, u_des: Array, *args, **kwargs) -> Array:
|
|
317
365
|
"""Quadratic term in the CBF QP objective (minimize 0.5 * x^T P x + q^T x)
|
|
318
366
|
|
|
319
367
|
This defaults to 2 * I, which is the value of P when minimizing the standard CBF objective,
|
|
@@ -324,14 +372,13 @@ class CBFConfig(ABC):
|
|
|
324
372
|
Args:
|
|
325
373
|
z (Array): State, shape (n,)
|
|
326
374
|
u_des (Array): Desired control input, shape (m,)
|
|
327
|
-
*h_args: Optional additional arguments for the barrier function.
|
|
328
375
|
|
|
329
376
|
Returns:
|
|
330
377
|
Array: P matrix, shape (m, m)
|
|
331
378
|
"""
|
|
332
379
|
return 2 * jnp.eye(self.m)
|
|
333
380
|
|
|
334
|
-
def q(self, z: Array, u_des: Array, *
|
|
381
|
+
def q(self, z: Array, u_des: Array, *args, **kwargs) -> Array:
|
|
335
382
|
"""Linear term in the CBF QP objective (minimize 0.5 * x^T P x + q^T x)
|
|
336
383
|
|
|
337
384
|
This defaults to -2 * u_des, which is the value of q when minimizing the standard CBF objective,
|
|
@@ -342,7 +389,6 @@ class CBFConfig(ABC):
|
|
|
342
389
|
Args:
|
|
343
390
|
z (Array): State, shape (n,)
|
|
344
391
|
u_des (Array): Desired control input, shape (m,)
|
|
345
|
-
*h_args: Optional additional arguments for the barrier function.
|
|
346
392
|
|
|
347
393
|
Returns:
|
|
348
394
|
Array: q vector, shape (m,)
|
|
@@ -352,7 +398,7 @@ class CBFConfig(ABC):
|
|
|
352
398
|
## Helper functions ##
|
|
353
399
|
|
|
354
400
|
def _check_class_kappa(
|
|
355
|
-
self, func: Callable[[ArrayLike], ArrayLike], dim: int
|
|
401
|
+
self, func: Callable[[ArrayLike], ArrayLike], dim: int, *args, **kwargs
|
|
356
402
|
) -> None:
|
|
357
403
|
"""Checks that the provided function is in class Kappa
|
|
358
404
|
|
|
@@ -361,16 +407,20 @@ class CBFConfig(ABC):
|
|
|
361
407
|
dim (int): Expected dimension of the output
|
|
362
408
|
"""
|
|
363
409
|
assert isinstance(func, Callable)
|
|
410
|
+
|
|
411
|
+
def func_wrapper(x):
|
|
412
|
+
return func(x, *args, **kwargs)
|
|
413
|
+
|
|
364
414
|
try:
|
|
365
415
|
# Check that func(0) == 0
|
|
366
|
-
assert
|
|
416
|
+
assert np.allclose(func_wrapper(np.zeros(dim)), 0.0)
|
|
367
417
|
# Check that func is monotonically increasing
|
|
368
418
|
n_test = 100
|
|
369
|
-
test_points =
|
|
370
|
-
|
|
419
|
+
test_points = np.repeat(
|
|
420
|
+
np.linspace(-1e6, 1e6, n_test).reshape(n_test, 1), dim, axis=1
|
|
371
421
|
)
|
|
372
|
-
a = jax.vmap(
|
|
373
|
-
assert
|
|
422
|
+
a = jax.vmap(func_wrapper, in_axes=0)(test_points)
|
|
423
|
+
assert np.all(a[:-1, :] < a[1:, :])
|
|
374
424
|
except AssertionError as e:
|
|
375
425
|
raise ValueError(
|
|
376
426
|
f"{func.__name__} does not appear to be a class Kappa function"
|
cbfpy/config/clf_cbf_config.py
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
## Defining the problem:
|
|
5
5
|
|
|
6
|
-
As with the CBF, we require implementation of the dynamics functions `f` and `g`, as well as the barrier function(s)
|
|
6
|
+
As with the CBF, we require implementation of the dynamics functions `f` and `g`, as well as the barrier function(s)
|
|
7
7
|
`h`. Now, with the CLF-CBF, we require the definition of the Control Lyapunov Function (CLF) `V`. This CLF must be a
|
|
8
|
-
positive definite function of the state.
|
|
8
|
+
positive definite function of the state.
|
|
9
9
|
|
|
10
|
-
Depending on the relative degree of your barrier function(s), you should implement the `h_1` method
|
|
10
|
+
Depending on the relative degree of your barrier function(s), you should implement the `h_1` method
|
|
11
11
|
(for a relative-degree-1 barrier), and/or the `h_2` method (for a relative-degree-2 barrier).
|
|
12
12
|
|
|
13
13
|
Likewise, for the CLF, you should implement the `V_1` method (for a relative-degree-1 CLF), and/or the `V_2` method
|
|
@@ -24,7 +24,7 @@ CLF objective. These can be used to adjust the weightings between inputs, for in
|
|
|
24
24
|
|
|
25
25
|
## Relaxation:
|
|
26
26
|
|
|
27
|
-
If the CBF constraints are not necessarily globally feasible, you can enable further relaxation in the CLFCBFConfig.
|
|
27
|
+
If the CBF constraints are not necessarily globally feasible, you can enable further relaxation in the CLFCBFConfig.
|
|
28
28
|
However, since the CLF constraint was already relaxed with respect to the CBF constraint, this means that tuning the
|
|
29
29
|
relaxation parameters is critical. In general, the penalty on the CBF relaxation should be much higher than the penalty
|
|
30
30
|
on the CLF relaxation.
|
|
@@ -35,6 +35,7 @@ is infeasible.
|
|
|
35
35
|
|
|
36
36
|
from typing import Optional
|
|
37
37
|
|
|
38
|
+
import numpy as np
|
|
38
39
|
import jax.numpy as jnp
|
|
39
40
|
from jax import Array
|
|
40
41
|
from jax.typing import ArrayLike
|
|
@@ -66,14 +67,18 @@ class CLFCBFConfig(CBFConfig):
|
|
|
66
67
|
m (int): Control dimension
|
|
67
68
|
u_min (ArrayLike, optional): Minimum control input, shape (m,). Defaults to None (Unconstrained).
|
|
68
69
|
u_max (ArrayLike, optional): Maximum control input, shape (m,). Defaults to None (Unconstrained).
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
relax_qp (bool, optional): Whether to allow for relaxation in the CLF-CBF QP. Defaults to True.
|
|
71
|
+
Note: this is required for differentiability through the QP.
|
|
72
|
+
cbf_relaxation_penalty (float, optional): Penalty on the slack variable in the relaxed QP. Defaults to 1e4.
|
|
73
|
+
Note: only applies if relax_qp is True.
|
|
72
74
|
clf_relaxation_penalty (float): Penalty on the CLF slack variable when enforcing the CBF. Defaults to 1e2
|
|
75
|
+
control_relaxation_penalty (float, optional): Penalty on the control constraint slack variables in the
|
|
76
|
+
relaxed QP. Defaults to 1e5. Note: only applies if relax_qp is True.
|
|
73
77
|
solver_tol (float, optional): Tolerance for the QP solver. Defaults to 1e-3.
|
|
74
|
-
init_args (tuple, optional): If your
|
|
75
|
-
include an initial seed for these
|
|
76
|
-
|
|
78
|
+
init_args (tuple, optional): If your barriers or dynamics rely on additional (non-differentiable, static shape)
|
|
79
|
+
args other than just the state, include an initial seed for these args here. Defaults to None.
|
|
80
|
+
init_kwargs (dict, optional): If your barriers or dynamics rely on additional (non-differentiable, static shape)
|
|
81
|
+
kwargs other than just the state, include an initial seed for these kwargs here. Defaults to None.
|
|
77
82
|
"""
|
|
78
83
|
|
|
79
84
|
def __init__(
|
|
@@ -82,21 +87,25 @@ class CLFCBFConfig(CBFConfig):
|
|
|
82
87
|
m: int,
|
|
83
88
|
u_min: Optional[ArrayLike] = None,
|
|
84
89
|
u_max: Optional[ArrayLike] = None,
|
|
85
|
-
|
|
90
|
+
relax_qp: bool = True,
|
|
86
91
|
cbf_relaxation_penalty: float = 1e4,
|
|
87
92
|
clf_relaxation_penalty: float = 1e2,
|
|
93
|
+
control_relaxation_penalty: float = 1e5,
|
|
88
94
|
solver_tol: float = 1e-3,
|
|
89
|
-
init_args: tuple =
|
|
95
|
+
init_args: Optional[tuple] = None,
|
|
96
|
+
init_kwargs: Optional[dict] = None,
|
|
90
97
|
):
|
|
91
98
|
super().__init__(
|
|
92
99
|
n,
|
|
93
100
|
m,
|
|
94
101
|
u_min,
|
|
95
102
|
u_max,
|
|
96
|
-
|
|
103
|
+
relax_qp,
|
|
97
104
|
cbf_relaxation_penalty,
|
|
105
|
+
control_relaxation_penalty,
|
|
98
106
|
solver_tol,
|
|
99
107
|
init_args,
|
|
108
|
+
init_kwargs,
|
|
100
109
|
)
|
|
101
110
|
|
|
102
111
|
if not (
|
|
@@ -108,10 +117,19 @@ class CLFCBFConfig(CBFConfig):
|
|
|
108
117
|
)
|
|
109
118
|
self.clf_relaxation_penalty = float(clf_relaxation_penalty)
|
|
110
119
|
|
|
120
|
+
# If relaxing the QP, need to have CLF penalty < CBF penalty < Control constraint penalty
|
|
121
|
+
if self.cbf_relaxation_penalty < self.clf_relaxation_penalty:
|
|
122
|
+
print("WARNING: CBF constraints have a lower penalty than the CLFs")
|
|
123
|
+
if (
|
|
124
|
+
self.control_constrained
|
|
125
|
+
and self.control_relaxation_penalty < self.clf_relaxation_penalty
|
|
126
|
+
):
|
|
127
|
+
print("WARNING: Control constraints have a lower penalty than the CLFs")
|
|
128
|
+
|
|
111
129
|
# Check on CLF dimension
|
|
112
|
-
z_test =
|
|
113
|
-
v1_test = self.V_1(
|
|
114
|
-
v2_test = self.V_2(
|
|
130
|
+
z_test = np.ones(self.n)
|
|
131
|
+
v1_test = self.V_1(z_test, z_test, *self.init_args, **self.init_kwargs)
|
|
132
|
+
v2_test = self.V_2(z_test, z_test, *self.init_args, **self.init_kwargs)
|
|
115
133
|
if v1_test.ndim != 1 or v2_test.ndim != 1:
|
|
116
134
|
raise ValueError("CLF(s) must output 1D arrays")
|
|
117
135
|
self.num_rd1_clf = v1_test.shape[0]
|
|
@@ -122,9 +140,9 @@ class CLFCBFConfig(CBFConfig):
|
|
|
122
140
|
"No Lyanpunov functions provided."
|
|
123
141
|
+ "\nYou can implement this via the V_1 and/or V_2 methods in your config class"
|
|
124
142
|
)
|
|
125
|
-
v_test =
|
|
126
|
-
gamma_test = self.gamma(v_test)
|
|
127
|
-
gamma_2_test = self.gamma_2(v2_test)
|
|
143
|
+
v_test = np.concatenate([v1_test, v2_test])
|
|
144
|
+
gamma_test = self.gamma(v_test, *self.init_args, **self.init_kwargs)
|
|
145
|
+
gamma_2_test = self.gamma_2(v2_test, *self.init_args, **self.init_kwargs)
|
|
128
146
|
if gamma_test.shape != (self.num_clf,):
|
|
129
147
|
raise ValueError(
|
|
130
148
|
f"Invalid shape for gamma(V(z)): {gamma_test.shape}. Expected ({self.num_clf},)"
|
|
@@ -135,18 +153,48 @@ class CLFCBFConfig(CBFConfig):
|
|
|
135
153
|
f"Invalid shape for gamma_2(V_2(z)): {gamma_2_test.shape}. Expected ({self.num_rd2_clf},)"
|
|
136
154
|
+ "\nCheck that the output of the gamma_2() function matches the number of RD2 CLFs"
|
|
137
155
|
)
|
|
138
|
-
self._check_class_kappa(
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
self._check_class_kappa(
|
|
157
|
+
self.gamma, self.num_clf, *self.init_args, **self.init_kwargs
|
|
158
|
+
)
|
|
159
|
+
self._check_class_kappa(
|
|
160
|
+
self.gamma_2, self.num_rd2_clf, *self.init_args, **self.init_kwargs
|
|
161
|
+
)
|
|
162
|
+
H_test = self.H(z_test, *self.init_args, **self.init_kwargs)
|
|
141
163
|
if H_test.shape != (self.m, self.m):
|
|
142
164
|
raise ValueError(
|
|
143
165
|
f"Invalid shape for H(z): {H_test.shape}. Expected ({self.m}, {self.m})"
|
|
144
166
|
)
|
|
145
167
|
if not self._is_symmetric_psd(H_test):
|
|
146
168
|
raise ValueError("H(z) must be symmetric positive semi-definite")
|
|
147
|
-
# TODO: add a warning if the CLF relaxation penalty > the QP relaxation penalty?
|
|
148
169
|
|
|
149
|
-
|
|
170
|
+
# Handle QP relaxation penalties, if relaxation is enabled
|
|
171
|
+
num_qp_constraints = (
|
|
172
|
+
self.num_cbf + self.num_clf
|
|
173
|
+
if not self.control_constrained
|
|
174
|
+
else self.num_cbf + self.num_clf + 2 * self.m
|
|
175
|
+
)
|
|
176
|
+
if self.control_constrained:
|
|
177
|
+
self.constraint_relaxation_penalties = tuple(
|
|
178
|
+
np.concatenate(
|
|
179
|
+
[
|
|
180
|
+
self.clf_relaxation_penalty * np.ones(self.num_clf),
|
|
181
|
+
self.cbf_relaxation_penalty * np.ones(self.num_cbf),
|
|
182
|
+
self.control_relaxation_penalty * np.ones(2 * self.m),
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
self.constraint_relaxation_penalties = tuple(
|
|
188
|
+
np.concatenate(
|
|
189
|
+
[
|
|
190
|
+
self.clf_relaxation_penalty * np.ones(self.num_clf),
|
|
191
|
+
self.cbf_relaxation_penalty * np.ones(self.num_cbf),
|
|
192
|
+
]
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
assert len(self.constraint_relaxation_penalties) == num_qp_constraints
|
|
196
|
+
|
|
197
|
+
def V_1(self, z: ArrayLike, z_des: ArrayLike, *args, **kwargs) -> Array:
|
|
150
198
|
"""Relative-Degree-1 Control Lyapunov Function (CLF)
|
|
151
199
|
|
|
152
200
|
A CLF is a positive-definite function which evaluates to zero at the equilibrium point, and is
|
|
@@ -161,6 +209,7 @@ class CLFCBFConfig(CBFConfig):
|
|
|
161
209
|
|
|
162
210
|
Args:
|
|
163
211
|
z (ArrayLike): State, shape (n,)
|
|
212
|
+
z_des (ArrayLike): Desired state, shape (n,)
|
|
164
213
|
|
|
165
214
|
Returns:
|
|
166
215
|
Array: V(z): The RD1 CLF evaluation, shape (num_rd1_clf,)
|
|
@@ -168,7 +217,7 @@ class CLFCBFConfig(CBFConfig):
|
|
|
168
217
|
return jnp.array([])
|
|
169
218
|
|
|
170
219
|
# TODO: Check if the math behind this is actually valid
|
|
171
|
-
def V_2(self, z: ArrayLike) -> Array:
|
|
220
|
+
def V_2(self, z: ArrayLike, z_des: ArrayLike, *args, **kwargs) -> Array:
|
|
172
221
|
"""Relative-Degree-2 (high-order) Control Lyapunov Function (CLF)
|
|
173
222
|
|
|
174
223
|
A CLF is a positive-definite function which evaluates to zero at the equilibrium point, and is
|
|
@@ -184,13 +233,14 @@ class CLFCBFConfig(CBFConfig):
|
|
|
184
233
|
|
|
185
234
|
Args:
|
|
186
235
|
z (ArrayLike): State, shape (n,)
|
|
236
|
+
z_des (ArrayLike): Desired state, shape (n,)
|
|
187
237
|
|
|
188
238
|
Returns:
|
|
189
239
|
Array: V(z): The RD2 CLF evaluation, shape (num_rd2_clf,)
|
|
190
240
|
"""
|
|
191
241
|
return jnp.array([])
|
|
192
242
|
|
|
193
|
-
def gamma(self, v: ArrayLike) -> Array:
|
|
243
|
+
def gamma(self, v: ArrayLike, *args, **kwargs) -> Array:
|
|
194
244
|
"""A class Kappa function, dictating the "gain" of the CLF
|
|
195
245
|
|
|
196
246
|
For reference, a class Kappa function is a monotonically increasing function which passes through the origin.
|
|
@@ -205,7 +255,7 @@ class CLFCBFConfig(CBFConfig):
|
|
|
205
255
|
"""
|
|
206
256
|
return v
|
|
207
257
|
|
|
208
|
-
def gamma_2(self, v_2: ArrayLike) -> Array:
|
|
258
|
+
def gamma_2(self, v_2: ArrayLike, *args, **kwargs) -> Array:
|
|
209
259
|
"""A second class Kappa function, dictating the "gain" associated with the derivative of the CLF
|
|
210
260
|
|
|
211
261
|
For reference, a class Kappa function is a monotonically increasing function which passes through the origin.
|
|
@@ -220,7 +270,7 @@ class CLFCBFConfig(CBFConfig):
|
|
|
220
270
|
"""
|
|
221
271
|
return v_2
|
|
222
272
|
|
|
223
|
-
def H(self, z: ArrayLike) -> Array:
|
|
273
|
+
def H(self, z: ArrayLike, *args, **kwargs) -> Array:
|
|
224
274
|
"""Matrix defining the quadratic control term in the CLF objective (minimize 0.5 * u^T H u + F^T u)
|
|
225
275
|
|
|
226
276
|
**Must be PSD!**
|
|
@@ -236,7 +286,7 @@ class CLFCBFConfig(CBFConfig):
|
|
|
236
286
|
"""
|
|
237
287
|
return jnp.eye(self.m)
|
|
238
288
|
|
|
239
|
-
def F(self, z: ArrayLike) -> Array:
|
|
289
|
+
def F(self, z: ArrayLike, *args, **kwargs) -> Array:
|
|
240
290
|
"""Vector defining the linear term in the CLF objective (minimize 0.5 * u^T H u + F^T u)
|
|
241
291
|
|
|
242
292
|
The default implementation is a zero vector, but this can be overridden
|
cbfpy/envs/base_env.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
This is a convenient structure for building demo environments to test CBFs. However, it is not necessary to use this.
|
|
5
5
|
|
|
6
|
-
For instance, going back to the CBF usage pseudocode,
|
|
6
|
+
For instance, going back to the CBF usage pseudocode,
|
|
7
7
|
```
|
|
8
8
|
while True:
|
|
9
9
|
z = get_state()
|
|
@@ -11,10 +11,10 @@ while True:
|
|
|
11
11
|
u_nom = nominal_controller(z, z_des)
|
|
12
12
|
u = cbf.safety_filter(z, u_nom)
|
|
13
13
|
apply_control(u)
|
|
14
|
-
step()
|
|
14
|
+
step()
|
|
15
15
|
```
|
|
16
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.
|
|
17
|
+
any derived environments.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
from abc import ABC, abstractmethod
|
cbfpy/envs/drone_env.py
CHANGED
|
@@ -49,10 +49,11 @@ class ACCConfig(CLFCBFConfig):
|
|
|
49
49
|
u_max=u_max,
|
|
50
50
|
# Note: Relaxing the CLF-CBF QP is tricky because there is an additional relaxation
|
|
51
51
|
# parameter already, balancing the CLF and CBF constraints.
|
|
52
|
-
|
|
53
|
-
# If indeed relaxing, ensure that the
|
|
52
|
+
relax_qp=False,
|
|
53
|
+
# If indeed relaxing, ensure that the CBF relaxation >> the CLF relaxation
|
|
54
54
|
clf_relaxation_penalty=10.0,
|
|
55
|
-
cbf_relaxation_penalty=
|
|
55
|
+
cbf_relaxation_penalty=1e5,
|
|
56
|
+
control_relaxation_penalty=1e6,
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
def drag_force(self, v: float) -> float:
|
|
@@ -82,7 +83,7 @@ class ACCConfig(CLFCBFConfig):
|
|
|
82
83
|
[D - self.T * v_f - 0.5 * (v_l - v_f) ** 2 / (self.cd * self.gravity)]
|
|
83
84
|
)
|
|
84
85
|
|
|
85
|
-
def V_1(self, z: ArrayLike) -> float:
|
|
86
|
+
def V_1(self, z: ArrayLike, z_des: ArrayLike) -> float:
|
|
86
87
|
# CLF: Squared error between the follower velocity and the desired velocity
|
|
87
88
|
return jnp.array([(z[0] - self.v_des) ** 2])
|
|
88
89
|
|
cbfpy/examples/drone_demo.py
CHANGED
|
@@ -38,20 +38,20 @@ class DroneConfig(CBFConfig):
|
|
|
38
38
|
super().__init__(
|
|
39
39
|
n=6, # State = [position, velocity]
|
|
40
40
|
m=3, # Control = [velocity]
|
|
41
|
-
|
|
41
|
+
relax_qp=True,
|
|
42
42
|
init_args=(init_z_obs,),
|
|
43
43
|
cbf_relaxation_penalty=1e6,
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
def f(self, z):
|
|
46
|
+
def f(self, z, *args, **kwargs):
|
|
47
47
|
# Assume we are directly controlling the robot's velocity
|
|
48
48
|
return jnp.zeros(self.n)
|
|
49
49
|
|
|
50
|
-
def g(self, z):
|
|
50
|
+
def g(self, z, *args, **kwargs):
|
|
51
51
|
# Assume we are directly controlling the robot's velocity
|
|
52
52
|
return jnp.block([[jnp.eye(3)], [jnp.zeros((3, 3))]])
|
|
53
53
|
|
|
54
|
-
def h_1(self, z, z_obs):
|
|
54
|
+
def h_1(self, z, z_obs, **kwargs):
|
|
55
55
|
obstacle_radius = 0.25
|
|
56
56
|
robot_radius = 0.25
|
|
57
57
|
padding = 0.1
|
|
@@ -73,10 +73,12 @@ class DroneConfig(CBFConfig):
|
|
|
73
73
|
- padding
|
|
74
74
|
]
|
|
75
75
|
)
|
|
76
|
-
h_box_containment = jnp.concatenate(
|
|
76
|
+
h_box_containment = jnp.concatenate(
|
|
77
|
+
[self.pos_max - z[:3], z[:3] - self.pos_min]
|
|
78
|
+
)
|
|
77
79
|
return jnp.concatenate([h_obstacle_avoidance, h_box_containment])
|
|
78
80
|
|
|
79
|
-
def alpha(self, h):
|
|
81
|
+
def alpha(self, h, *args, **kwargs):
|
|
80
82
|
return jnp.array([3.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) * h
|
|
81
83
|
|
|
82
84
|
|