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,401 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# CBF Configuration class
|
|
3
|
+
|
|
4
|
+
## Defining the problem:
|
|
5
|
+
|
|
6
|
+
CBFs have two primary implementation requirements: the dynamics functions, and the barrier function(s).
|
|
7
|
+
These can be specified through the `f`, `g`, and `h` methods, respectively. Note that the main requirements
|
|
8
|
+
for these functions are that (1) the dynamics are control-affine, and (2) the barrier function(s) are "zeroing"
|
|
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.
|
|
11
|
+
|
|
12
|
+
Depending on the relative degree of your barrier function(s), you should implement the `h_1` method
|
|
13
|
+
(for a relative-degree-1 barrier), and/or the `h_2` method (for a relative-degree-2 barrier).
|
|
14
|
+
|
|
15
|
+
## Tuning the CBF:
|
|
16
|
+
|
|
17
|
+
The CBF config provides a default implementation of the CBF "gain" function `alpha`, and `alpha_2` for
|
|
18
|
+
relative-degree-2 barriers. To change the sensitivity of the CBF, these functions can be modified to
|
|
19
|
+
increase or decrease the effect of the barrier(s). For instance, `alpha(h) = h` is the default implementation,
|
|
20
|
+
but to increase the sensitivity of the CBF, one could use `alpha(h) = 2 * h`. The only requirements for these
|
|
21
|
+
functions are that they are monotonically increasing and pass through the origin (class Kappa functions).
|
|
22
|
+
|
|
23
|
+
The CBFConfig also provides a default implementation of the CBF QP objective function, which is to minimize
|
|
24
|
+
the norm of the difference between the safe control input and the desired control input. This can also be modified
|
|
25
|
+
through the `P` and `q` methods, which define the quadratic and linear terms in the QP objective, respectively. This
|
|
26
|
+
does require that P is positive semi-definite.
|
|
27
|
+
|
|
28
|
+
## Relaxation:
|
|
29
|
+
|
|
30
|
+
Depending on the construction of the barrier functions and if control limits are provided, the CBF QP may not always be
|
|
31
|
+
feasible. If allowing for relaxation in the CBFConfig, a slack variable will be introduced to ensure that the
|
|
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.
|
|
34
|
+
|
|
35
|
+
If strict enforcement of the CBF is desired, your higest-level controller should handle the case where the QP
|
|
36
|
+
is infeasible.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from typing import Optional, Callable
|
|
40
|
+
from abc import ABC, abstractmethod
|
|
41
|
+
|
|
42
|
+
import numpy as np
|
|
43
|
+
import jax
|
|
44
|
+
import jax.numpy as jnp
|
|
45
|
+
from jax import Array
|
|
46
|
+
from jax.typing import ArrayLike
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CBFConfig(ABC):
|
|
50
|
+
"""Control Barrier Function (CBF) configuration class.
|
|
51
|
+
|
|
52
|
+
This is an abstract class which requires implementation of the following methods:
|
|
53
|
+
|
|
54
|
+
- `f(z)`: The uncontrolled dynamics function
|
|
55
|
+
- `g(z)`: The control affine dynamics function
|
|
56
|
+
- `h_1(z)` and/or `h_2(z)`: The barrier function(s), of relative degree 1 and/or 2
|
|
57
|
+
|
|
58
|
+
For finer-grained control over the CBF, the following methods may be updated from their defaults:
|
|
59
|
+
|
|
60
|
+
- `alpha(h)`: "Gain" of the CBF
|
|
61
|
+
- `alpha_2(h_2)`: "Gain" of the relative-degree-2 CBFs, if applicable
|
|
62
|
+
- `P(z, u_des)`: Quadratic term in the CBF QP objective
|
|
63
|
+
- `q(z, u_des)`: Linear term in the CBF QP objective
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
n (int): State dimension
|
|
67
|
+
m (int): Control dimension
|
|
68
|
+
u_min (ArrayLike, optional): Minimum control input, shape (m,). Defaults to None (Unconstrained).
|
|
69
|
+
u_max (ArrayLike, optional): Maximum control input, shape (m,). Defaults to None (Unconstrained).
|
|
70
|
+
relax_cbf (bool, optional): Whether to allow for relaxation in the CBF QP. Defaults to True.
|
|
71
|
+
cbf_relaxation_penalty (float, optional): Penalty on the slack variable in the relaxed CBF QP. Defaults to 1e3.
|
|
72
|
+
Note: only applies if relax_cbf is True.
|
|
73
|
+
solver_tol (float, optional): Tolerance for the QP solver. Defaults to 1e-3.
|
|
74
|
+
init_args (tuple, optional): If your barrier function relies on additional arguments other than just the state,
|
|
75
|
+
include an initial seed for these arguments here. This is to help test the output of the barrier function.
|
|
76
|
+
Defaults to ().
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
n: int,
|
|
82
|
+
m: int,
|
|
83
|
+
u_min: Optional[ArrayLike] = None,
|
|
84
|
+
u_max: Optional[ArrayLike] = None,
|
|
85
|
+
relax_cbf: bool = True,
|
|
86
|
+
cbf_relaxation_penalty: float = 1e3,
|
|
87
|
+
solver_tol: float = 1e-3,
|
|
88
|
+
init_args: tuple = (),
|
|
89
|
+
):
|
|
90
|
+
if not (isinstance(n, int) and n > 0):
|
|
91
|
+
raise ValueError(f"n must be a positive integer. Got: {n}")
|
|
92
|
+
self.n = n
|
|
93
|
+
|
|
94
|
+
if not (isinstance(m, int) and m > 0):
|
|
95
|
+
raise ValueError(f"m must be a positive integer. Got: {m}")
|
|
96
|
+
self.m = m
|
|
97
|
+
|
|
98
|
+
if not isinstance(relax_cbf, bool):
|
|
99
|
+
raise ValueError(f"relax_cbf must be a boolean. Got: {relax_cbf}")
|
|
100
|
+
self.relax_cbf = relax_cbf
|
|
101
|
+
|
|
102
|
+
if not (
|
|
103
|
+
isinstance(cbf_relaxation_penalty, (int, float))
|
|
104
|
+
and cbf_relaxation_penalty >= 0
|
|
105
|
+
):
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"CBF relaxation penalty must be a non-negative value. Got: {cbf_relaxation_penalty}"
|
|
108
|
+
)
|
|
109
|
+
self.cbf_relaxation_penalty = float(cbf_relaxation_penalty)
|
|
110
|
+
|
|
111
|
+
if not (isinstance(solver_tol, (int, float)) and solver_tol > 0):
|
|
112
|
+
raise ValueError(f"solver_tol must be a positive value. Got: {solver_tol}")
|
|
113
|
+
self.solver_tol = float(solver_tol)
|
|
114
|
+
|
|
115
|
+
if not isinstance(init_args, tuple):
|
|
116
|
+
raise ValueError(f"init_args must be a tuple. Got: {init_args}")
|
|
117
|
+
self.init_args = init_args
|
|
118
|
+
|
|
119
|
+
# Control limits require a bit of extra handling. They can be both None if unconstrained,
|
|
120
|
+
# but we should not have one limit as None and the other as some value
|
|
121
|
+
u_min = np.asarray(u_min, dtype=float).flatten() if u_min is not None else None
|
|
122
|
+
u_max = np.asarray(u_max, dtype=float).flatten() if u_max is not None else None
|
|
123
|
+
if u_min is not None or u_max is not None:
|
|
124
|
+
self.control_constrained = True
|
|
125
|
+
if u_min is None and u_max is not None:
|
|
126
|
+
u_min = -np.inf * np.ones(self.m)
|
|
127
|
+
elif u_min is not None and u_max is None:
|
|
128
|
+
u_max = np.inf * np.ones(self.m)
|
|
129
|
+
else:
|
|
130
|
+
self.control_constrained = False
|
|
131
|
+
if u_min is not None:
|
|
132
|
+
assert u_min.shape == (self.m,)
|
|
133
|
+
u_min = tuple(u_min)
|
|
134
|
+
if u_max is not None:
|
|
135
|
+
assert u_max.shape == (self.m,)
|
|
136
|
+
u_max = tuple(u_max)
|
|
137
|
+
self.u_min = u_min
|
|
138
|
+
self.u_max = u_max
|
|
139
|
+
|
|
140
|
+
# Test if the methods are provided and verify their output dimension
|
|
141
|
+
z_test = jnp.ones(self.n)
|
|
142
|
+
u_test = jnp.ones(self.m)
|
|
143
|
+
f_test = self.f(z_test)
|
|
144
|
+
g_test = self.g(z_test)
|
|
145
|
+
if f_test.shape != (self.n,):
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"Invalid shape for f(z). Got {f_test.shape}, expected ({self.n},)"
|
|
148
|
+
)
|
|
149
|
+
if g_test.shape != (self.n, self.m):
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"Invalid shape for g(z). Got {g_test.shape}, expected ({self.n}, {self.m})"
|
|
152
|
+
)
|
|
153
|
+
try:
|
|
154
|
+
h1_test = self.h_1(z_test, *self.init_args)
|
|
155
|
+
h2_test = self.h_2(z_test, *self.init_args)
|
|
156
|
+
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
|
|
161
|
+
if h1_test.ndim != 1 or h2_test.ndim != 1:
|
|
162
|
+
raise ValueError("Barrier function(s) must be 1D arrays")
|
|
163
|
+
self.num_rd1_cbf = h1_test.shape[0]
|
|
164
|
+
self.num_rd2_cbf = h2_test.shape[0]
|
|
165
|
+
self.num_cbf = self.num_rd1_cbf + self.num_rd2_cbf
|
|
166
|
+
if self.num_cbf == 0:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"No barrier functions provided."
|
|
169
|
+
+ "\nYou can implement this via the h_1 and/or h_2 methods in your config class"
|
|
170
|
+
)
|
|
171
|
+
h_test = jnp.concatenate([h1_test, h2_test])
|
|
172
|
+
alpha_test = self.alpha(h_test)
|
|
173
|
+
alpha_2_test = self.alpha_2(h2_test)
|
|
174
|
+
if alpha_test.shape != (self.num_cbf,):
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Invalid shape for alpha(h(z)): {alpha_test.shape}. Expected ({self.num_cbf},)"
|
|
177
|
+
+ "\nCheck that the output of the alpha() function matches the number of CBFs"
|
|
178
|
+
)
|
|
179
|
+
if alpha_2_test.shape != (self.num_rd2_cbf,):
|
|
180
|
+
raise ValueError(
|
|
181
|
+
f"Invalid shape for alpha_2(h_2(z)): {alpha_2_test.shape}. Expected ({self.num_rd2_cbf},)"
|
|
182
|
+
+ "\nCheck that the output of the alpha_2() function matches the number of RD2 CBFs"
|
|
183
|
+
)
|
|
184
|
+
self._check_class_kappa(self.alpha, self.num_cbf)
|
|
185
|
+
self._check_class_kappa(self.alpha_2, self.num_rd2_cbf)
|
|
186
|
+
try:
|
|
187
|
+
P_test = self.P(z_test, u_test, *self.init_args)
|
|
188
|
+
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
|
|
193
|
+
if P_test.shape != (self.m, self.m):
|
|
194
|
+
raise ValueError(
|
|
195
|
+
f"Invalid shape for P(z). Got {P_test.shape}, expected ({self.m}, {self.m})"
|
|
196
|
+
)
|
|
197
|
+
if not self._is_symmetric_psd(P_test):
|
|
198
|
+
raise ValueError("P matrix must be symmetric positive semi-definite")
|
|
199
|
+
|
|
200
|
+
## Control Affine Dynamics ##
|
|
201
|
+
|
|
202
|
+
@abstractmethod
|
|
203
|
+
def f(self, z: ArrayLike) -> Array:
|
|
204
|
+
"""The uncontrolled dynamics function. Possibly nonlinear, and locally Lipschitz
|
|
205
|
+
|
|
206
|
+
i.e. the function f, such that z_dot = f(z) + g(z) u
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
z (ArrayLike): The state, shape (n,)
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Array: Uncontrolled state derivative component, shape (n,)
|
|
213
|
+
"""
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
@abstractmethod
|
|
217
|
+
def g(self, z: ArrayLike) -> Array:
|
|
218
|
+
"""The control affine dynamics function. Locally Lipschitz.
|
|
219
|
+
|
|
220
|
+
i.e. the function g, such that z_dot = f(z) + g(z) u
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
z (ArrayLike): The state, shape (n,)
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Array: Control matrix, shape (n, m)
|
|
227
|
+
"""
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
## Barriers ##
|
|
231
|
+
|
|
232
|
+
def h_1(self, z: ArrayLike, *h_args) -> Array:
|
|
233
|
+
"""Relative-degree-1 barrier function(s).
|
|
234
|
+
|
|
235
|
+
A (zeroing) CBF is a continuously-differentiable function h, such that for any state z in the interior of
|
|
236
|
+
the safe set, h(z) should be > 0, and h(z) = 0 on the boundary. When in the unsafe set, h(z) < 0.
|
|
237
|
+
|
|
238
|
+
Relative degree can generally be thought of as the number of integrations required between the
|
|
239
|
+
input and output of the system. For instance, a (relative-degree-1) CBF based on velocities,
|
|
240
|
+
with acceleration inputs, will be directly modified on the next timestep.
|
|
241
|
+
|
|
242
|
+
If your barrier function is relative-degree-2, or if you would like to enforce additional barriers
|
|
243
|
+
which are relative-degree-2, use the `h_2` method.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
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
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Array: Barrier function(s), shape (num_rd1_barr,)
|
|
252
|
+
"""
|
|
253
|
+
return jnp.array([])
|
|
254
|
+
|
|
255
|
+
def h_2(self, z: ArrayLike, *h_args) -> Array:
|
|
256
|
+
"""Relative-degree-2 (high-order) barrier function(s).
|
|
257
|
+
|
|
258
|
+
A (zeroing) CBF is a continuously-differentiable function h, such that for any state z in the interior of
|
|
259
|
+
the safe set, h(z) should be > 0, and h(z) = 0 on the boundary. When in the unsafe set, h(z) < 0.
|
|
260
|
+
|
|
261
|
+
Relative degree can generally be thought of as the number of integrations required between the
|
|
262
|
+
input and output of the system. For instance, a (relative-degree-2) CBF based on position,
|
|
263
|
+
with acceleration inputs, will be modified in two timesteps: the acceleration changes the velocity,
|
|
264
|
+
which then changes the position.
|
|
265
|
+
|
|
266
|
+
If your barrier function is relative-degree-1, or if you would like to enforce additional barriers
|
|
267
|
+
which are relative-degree-1, use the `h_1` method.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
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
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Array: Barrier function(s), shape (num_rd2_barr,)
|
|
276
|
+
"""
|
|
277
|
+
return jnp.array([])
|
|
278
|
+
|
|
279
|
+
## Additional tuning functions ##
|
|
280
|
+
|
|
281
|
+
def alpha(self, h: ArrayLike) -> Array:
|
|
282
|
+
"""A class Kappa function, dictating the "gain" of the barrier function(s)
|
|
283
|
+
|
|
284
|
+
For reference, a class Kappa function is a monotonically increasing function which passes through the origin.
|
|
285
|
+
A simple example is alpha(h) = h
|
|
286
|
+
|
|
287
|
+
The default implementation can be overridden for more fine-grained control over the CBF
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
h (ArrayLike): Evaluation of the barrier function(s) at the current state, shape (num_cbf,)
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Array: alpha(h(z)), shape (num_cbf,)
|
|
294
|
+
"""
|
|
295
|
+
return h
|
|
296
|
+
|
|
297
|
+
def alpha_2(self, h_2: ArrayLike) -> Array:
|
|
298
|
+
"""A second class Kappa function which dictactes the "gain" associated with the relative-degree-2
|
|
299
|
+
barrier functions
|
|
300
|
+
|
|
301
|
+
For reference, a class Kappa function is a monotonically increasing function which passes through the origin.
|
|
302
|
+
A simple example is alpha_2(h_2) = h_2
|
|
303
|
+
|
|
304
|
+
The default implementation can be overridden for more fine-grained control over the CBF
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
h_2 (ArrayLike): Evaluation of the RD2 barrier function(s) at the current state, shape (num_rd2_cbf,)
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Array: alpha_2(h_2(z)), shape (num_rd2_cbf,).
|
|
311
|
+
"""
|
|
312
|
+
return h_2
|
|
313
|
+
|
|
314
|
+
# Objective function tuning
|
|
315
|
+
|
|
316
|
+
def P(self, z: Array, u_des: Array, *h_args) -> Array:
|
|
317
|
+
"""Quadratic term in the CBF QP objective (minimize 0.5 * x^T P x + q^T x)
|
|
318
|
+
|
|
319
|
+
This defaults to 2 * I, which is the value of P when minimizing the standard CBF objective,
|
|
320
|
+
||u - u_des||_{2}^{2}
|
|
321
|
+
|
|
322
|
+
To change the objective, override this method. **Note that P must be PSD**
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
z (Array): State, shape (n,)
|
|
326
|
+
u_des (Array): Desired control input, shape (m,)
|
|
327
|
+
*h_args: Optional additional arguments for the barrier function.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Array: P matrix, shape (m, m)
|
|
331
|
+
"""
|
|
332
|
+
return 2 * jnp.eye(self.m)
|
|
333
|
+
|
|
334
|
+
def q(self, z: Array, u_des: Array, *h_args) -> Array:
|
|
335
|
+
"""Linear term in the CBF QP objective (minimize 0.5 * x^T P x + q^T x)
|
|
336
|
+
|
|
337
|
+
This defaults to -2 * u_des, which is the value of q when minimizing the standard CBF objective,
|
|
338
|
+
||u - u_des||_{2}^{2}
|
|
339
|
+
|
|
340
|
+
To change the objective, override this method.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
z (Array): State, shape (n,)
|
|
344
|
+
u_des (Array): Desired control input, shape (m,)
|
|
345
|
+
*h_args: Optional additional arguments for the barrier function.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Array: q vector, shape (m,)
|
|
349
|
+
"""
|
|
350
|
+
return -2 * u_des
|
|
351
|
+
|
|
352
|
+
## Helper functions ##
|
|
353
|
+
|
|
354
|
+
def _check_class_kappa(
|
|
355
|
+
self, func: Callable[[ArrayLike], ArrayLike], dim: int
|
|
356
|
+
) -> None:
|
|
357
|
+
"""Checks that the provided function is in class Kappa
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
func (Callable): Function to check
|
|
361
|
+
dim (int): Expected dimension of the output
|
|
362
|
+
"""
|
|
363
|
+
assert isinstance(func, Callable)
|
|
364
|
+
try:
|
|
365
|
+
# Check that func(0) == 0
|
|
366
|
+
assert jnp.allclose(func(jnp.zeros(dim)), 0.0)
|
|
367
|
+
# Check that func is monotonically increasing
|
|
368
|
+
n_test = 100
|
|
369
|
+
test_points = jnp.repeat(
|
|
370
|
+
jnp.linspace(-1e6, 1e6, n_test).reshape(n_test, 1), dim, axis=1
|
|
371
|
+
)
|
|
372
|
+
a = jax.vmap(func, in_axes=0)(test_points)
|
|
373
|
+
assert jnp.all(a[:-1, :] < a[1:, :])
|
|
374
|
+
except AssertionError as e:
|
|
375
|
+
raise ValueError(
|
|
376
|
+
f"{func.__name__} does not appear to be a class Kappa function"
|
|
377
|
+
) from e
|
|
378
|
+
|
|
379
|
+
def _is_symmetric_psd(self, mat: Array) -> bool:
|
|
380
|
+
"""Check that a matrix is symmetric positive semi-definite
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
mat (Array): Matrix to check
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
bool: True if the matrix is symmetric PSD, False otherwise
|
|
387
|
+
"""
|
|
388
|
+
mat = np.asarray(mat)
|
|
389
|
+
# Must be square
|
|
390
|
+
if mat.shape[0] != mat.shape[1]:
|
|
391
|
+
return False
|
|
392
|
+
# Must be symmetric or hermitian
|
|
393
|
+
if not np.allclose(mat, mat.conj().T, atol=1e-14):
|
|
394
|
+
return False
|
|
395
|
+
# Check PSD with cholesky. Cholesky can only tell if a matrix is PD, not PSD,
|
|
396
|
+
# but adding a small regularization term will allow this test to work
|
|
397
|
+
try:
|
|
398
|
+
np.linalg.cholesky(mat + np.eye(mat.shape[0]) * 1e-14)
|
|
399
|
+
except np.linalg.LinAlgError:
|
|
400
|
+
return False
|
|
401
|
+
return True
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# CLF-CBF Configuration class
|
|
3
|
+
|
|
4
|
+
## Defining the problem:
|
|
5
|
+
|
|
6
|
+
As with the CBF, we require implementation of the dynamics functions `f` and `g`, as well as the barrier function(s)
|
|
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.
|
|
9
|
+
|
|
10
|
+
Depending on the relative degree of your barrier function(s), you should implement the `h_1` method
|
|
11
|
+
(for a relative-degree-1 barrier), and/or the `h_2` method (for a relative-degree-2 barrier).
|
|
12
|
+
|
|
13
|
+
Likewise, for the CLF, you should implement the `V_1` method (for a relative-degree-1 CLF), and/or the `V_2` method
|
|
14
|
+
(for a relative-degree-2 CLF).
|
|
15
|
+
|
|
16
|
+
## Tuning the CLF-CBF:
|
|
17
|
+
|
|
18
|
+
As with the CBF, the CLF-CBF config allows for adjustment of the class-Kappa CBF "gain" functions `alpha` and `alpha_2`.
|
|
19
|
+
Additionally, the CLF-CBF config allows for adjustment of the class-Kappa CLF "gain" functions `gamma` and `gamma_2`
|
|
20
|
+
(for relative-degree-2 CLFs).
|
|
21
|
+
|
|
22
|
+
The CLF-CBF config also allows for adjustment of the quadratic control term `H` and the linear control term `F` in the
|
|
23
|
+
CLF objective. These can be used to adjust the weightings between inputs, for instance.
|
|
24
|
+
|
|
25
|
+
## Relaxation:
|
|
26
|
+
|
|
27
|
+
If the CBF constraints are not necessarily globally feasible, you can enable further relaxation in the CLFCBFConfig.
|
|
28
|
+
However, since the CLF constraint was already relaxed with respect to the CBF constraint, this means that tuning the
|
|
29
|
+
relaxation parameters is critical. In general, the penalty on the CBF relaxation should be much higher than the penalty
|
|
30
|
+
on the CLF relaxation.
|
|
31
|
+
|
|
32
|
+
If strict enforcement of the CLF-CBF is desired, your higest-level controller should handle the case where the QP
|
|
33
|
+
is infeasible.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from typing import Optional
|
|
37
|
+
|
|
38
|
+
import jax.numpy as jnp
|
|
39
|
+
from jax import Array
|
|
40
|
+
from jax.typing import ArrayLike
|
|
41
|
+
|
|
42
|
+
from cbfpy.config.cbf_config import CBFConfig
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CLFCBFConfig(CBFConfig):
|
|
46
|
+
"""Control Lyapunov Function / Control Barrier Function (CLF-CBF) configuration class.
|
|
47
|
+
|
|
48
|
+
This is an abstract class which requires implementation of the following methods:
|
|
49
|
+
|
|
50
|
+
- `f(z)`: The uncontrolled dynamics function
|
|
51
|
+
- `g(z)`: The control affine dynamics function
|
|
52
|
+
- `h_1(z)` and/or `h_2(z)`: The barrier function(s), of relative degree 1 and/or 2
|
|
53
|
+
- `V_1(z)` and/or `V_2(z)`: The Lyapunov function(s), of relative degree 1 and/or 2
|
|
54
|
+
|
|
55
|
+
For finer-grained control over the CLF-CBF, the following methods may be updated from their defaults:
|
|
56
|
+
|
|
57
|
+
- `alpha(h)`: "Gain" of the CBF
|
|
58
|
+
- `alpha_2(h_2)`: "Gain" of the relative-degree-2 CBFs, if applicable
|
|
59
|
+
- `gamma(v)`: "Gain" of the CLF
|
|
60
|
+
- `gamma_2(v)`: "Gain" of the relative-degree-2 CLFs, if applicable
|
|
61
|
+
- `H(z)`: Quadratic control term in the CLF objective
|
|
62
|
+
- `F(z)`: Linear control term in the CLF objective
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
n (int): State dimension
|
|
66
|
+
m (int): Control dimension
|
|
67
|
+
u_min (ArrayLike, optional): Minimum control input, shape (m,). Defaults to None (Unconstrained).
|
|
68
|
+
u_max (ArrayLike, optional): Maximum control input, shape (m,). Defaults to None (Unconstrained).
|
|
69
|
+
relax_cbf (bool, optional): Whether to allow for relaxation in the CBF QP. Defaults to True.
|
|
70
|
+
cbf_relaxation_penalty (float, optional): Penalty on the slack variable in the relaxed CBF QP. Defaults to 1e4.
|
|
71
|
+
Note: only applies if relax_cbf is True.
|
|
72
|
+
clf_relaxation_penalty (float): Penalty on the CLF slack variable when enforcing the CBF. Defaults to 1e2
|
|
73
|
+
solver_tol (float, optional): Tolerance for the QP solver. Defaults to 1e-3.
|
|
74
|
+
init_args (tuple, optional): If your barrier function relies on additional arguments other than just the state,
|
|
75
|
+
include an initial seed for these arguments here. This is to help test the output of the barrier function.
|
|
76
|
+
Defaults to ().
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
n: int,
|
|
82
|
+
m: int,
|
|
83
|
+
u_min: Optional[ArrayLike] = None,
|
|
84
|
+
u_max: Optional[ArrayLike] = None,
|
|
85
|
+
relax_cbf: bool = True,
|
|
86
|
+
cbf_relaxation_penalty: float = 1e4,
|
|
87
|
+
clf_relaxation_penalty: float = 1e2,
|
|
88
|
+
solver_tol: float = 1e-3,
|
|
89
|
+
init_args: tuple = (),
|
|
90
|
+
):
|
|
91
|
+
super().__init__(
|
|
92
|
+
n,
|
|
93
|
+
m,
|
|
94
|
+
u_min,
|
|
95
|
+
u_max,
|
|
96
|
+
relax_cbf,
|
|
97
|
+
cbf_relaxation_penalty,
|
|
98
|
+
solver_tol,
|
|
99
|
+
init_args,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if not (
|
|
103
|
+
isinstance(clf_relaxation_penalty, (int, float))
|
|
104
|
+
and clf_relaxation_penalty > 0
|
|
105
|
+
):
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Invalid clf_relaxation_penalty: {clf_relaxation_penalty}. Must be a positive value."
|
|
108
|
+
)
|
|
109
|
+
self.clf_relaxation_penalty = float(clf_relaxation_penalty)
|
|
110
|
+
|
|
111
|
+
# Check on CLF dimension
|
|
112
|
+
z_test = jnp.ones(self.n)
|
|
113
|
+
v1_test = self.V_1(jnp.ones(self.n))
|
|
114
|
+
v2_test = self.V_2(jnp.ones(self.n))
|
|
115
|
+
if v1_test.ndim != 1 or v2_test.ndim != 1:
|
|
116
|
+
raise ValueError("CLF(s) must output 1D arrays")
|
|
117
|
+
self.num_rd1_clf = v1_test.shape[0]
|
|
118
|
+
self.num_rd2_clf = v2_test.shape[0]
|
|
119
|
+
self.num_clf = self.num_rd1_clf + self.num_rd2_clf
|
|
120
|
+
if self.num_clf == 0:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
"No Lyanpunov functions provided."
|
|
123
|
+
+ "\nYou can implement this via the V_1 and/or V_2 methods in your config class"
|
|
124
|
+
)
|
|
125
|
+
v_test = jnp.concatenate([v1_test, v2_test])
|
|
126
|
+
gamma_test = self.gamma(v_test)
|
|
127
|
+
gamma_2_test = self.gamma_2(v2_test)
|
|
128
|
+
if gamma_test.shape != (self.num_clf,):
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Invalid shape for gamma(V(z)): {gamma_test.shape}. Expected ({self.num_clf},)"
|
|
131
|
+
+ "\nCheck that the output of the gamma() function matches the number of CLFs"
|
|
132
|
+
)
|
|
133
|
+
if gamma_2_test.shape != (self.num_rd2_clf,):
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"Invalid shape for gamma_2(V_2(z)): {gamma_2_test.shape}. Expected ({self.num_rd2_clf},)"
|
|
136
|
+
+ "\nCheck that the output of the gamma_2() function matches the number of RD2 CLFs"
|
|
137
|
+
)
|
|
138
|
+
self._check_class_kappa(self.gamma, self.num_clf)
|
|
139
|
+
self._check_class_kappa(self.gamma_2, self.num_rd2_clf)
|
|
140
|
+
H_test = self.H(z_test)
|
|
141
|
+
if H_test.shape != (self.m, self.m):
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"Invalid shape for H(z): {H_test.shape}. Expected ({self.m}, {self.m})"
|
|
144
|
+
)
|
|
145
|
+
if not self._is_symmetric_psd(H_test):
|
|
146
|
+
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
|
+
|
|
149
|
+
def V_1(self, z: ArrayLike) -> Array:
|
|
150
|
+
"""Relative-Degree-1 Control Lyapunov Function (CLF)
|
|
151
|
+
|
|
152
|
+
A CLF is a positive-definite function which evaluates to zero at the equilibrium point, and is
|
|
153
|
+
such that there exists a control input u which makes the time-derivative of the CLF negative.
|
|
154
|
+
|
|
155
|
+
Relative degree can generally be thought of as the number of integrations required between the
|
|
156
|
+
input and output of the system. For instance, a (relative-degree-1) CLF based on velocities,
|
|
157
|
+
with acceleration inputs, will be directly modified on the next timestep.
|
|
158
|
+
|
|
159
|
+
At least one of `V_1` or `V_2` must be implemented. Multiple CLFs is possible, but generally, these cannot all
|
|
160
|
+
be strictly enforced.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
z (ArrayLike): State, shape (n,)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Array: V(z): The RD1 CLF evaluation, shape (num_rd1_clf,)
|
|
167
|
+
"""
|
|
168
|
+
return jnp.array([])
|
|
169
|
+
|
|
170
|
+
# TODO: Check if the math behind this is actually valid
|
|
171
|
+
def V_2(self, z: ArrayLike) -> Array:
|
|
172
|
+
"""Relative-Degree-2 (high-order) Control Lyapunov Function (CLF)
|
|
173
|
+
|
|
174
|
+
A CLF is a positive-definite function which evaluates to zero at the equilibrium point, and is
|
|
175
|
+
such that there exists a control input u which makes the time-derivative of the CLF negative.
|
|
176
|
+
|
|
177
|
+
Relative degree can generally be thought of as the number of integrations required between the
|
|
178
|
+
input and output of the system. For instance, a (relative-degree-2) CLF based on position,
|
|
179
|
+
with acceleration inputs, will be modified in two timesteps: the acceleration changes the velocity,
|
|
180
|
+
which then changes the position.
|
|
181
|
+
|
|
182
|
+
At least one of `V_1` or `V_2` must be implemented. Multiple CLFs is possible, but generally, these cannot all
|
|
183
|
+
be strictly enforced.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
z (ArrayLike): State, shape (n,)
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Array: V(z): The RD2 CLF evaluation, shape (num_rd2_clf,)
|
|
190
|
+
"""
|
|
191
|
+
return jnp.array([])
|
|
192
|
+
|
|
193
|
+
def gamma(self, v: ArrayLike) -> Array:
|
|
194
|
+
"""A class Kappa function, dictating the "gain" of the CLF
|
|
195
|
+
|
|
196
|
+
For reference, a class Kappa function is a monotonically increasing function which passes through the origin.
|
|
197
|
+
|
|
198
|
+
The default implementation can be overridden for more fine-grained control over the CLF
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
v (ArrayLike): Evaluation of the CLF(s) at the current state, shape (num_clf,).
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Array: gamma(V(z)), shape (num_clf,).
|
|
205
|
+
"""
|
|
206
|
+
return v
|
|
207
|
+
|
|
208
|
+
def gamma_2(self, v_2: ArrayLike) -> Array:
|
|
209
|
+
"""A second class Kappa function, dictating the "gain" associated with the derivative of the CLF
|
|
210
|
+
|
|
211
|
+
For reference, a class Kappa function is a monotonically increasing function which passes through the origin.
|
|
212
|
+
|
|
213
|
+
The default implementation can be overridden for more fine-grained control over the CLF
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
v_2 (ArrayLike): Evaluation of the RD2 CLF(s) at the current state, shape (num_rd2_clf,)
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Array: gamma_2(V_2(z)), shape (num_rd2_clf,)
|
|
220
|
+
"""
|
|
221
|
+
return v_2
|
|
222
|
+
|
|
223
|
+
def H(self, z: ArrayLike) -> Array:
|
|
224
|
+
"""Matrix defining the quadratic control term in the CLF objective (minimize 0.5 * u^T H u + F^T u)
|
|
225
|
+
|
|
226
|
+
**Must be PSD!**
|
|
227
|
+
|
|
228
|
+
The default implementation is just the (m x m) identity matrix, but this can be overridden
|
|
229
|
+
for more fine-grained control over the objective
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
z (ArrayLike): State, shape (n,)
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Array: H, shape (m, m)
|
|
236
|
+
"""
|
|
237
|
+
return jnp.eye(self.m)
|
|
238
|
+
|
|
239
|
+
def F(self, z: ArrayLike) -> Array:
|
|
240
|
+
"""Vector defining the linear term in the CLF objective (minimize 0.5 * u^T H u + F^T u)
|
|
241
|
+
|
|
242
|
+
The default implementation is a zero vector, but this can be overridden
|
|
243
|
+
for more fine-grained control over the objective
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
z (ArrayLike): State, shape (n,)
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Array: F, shape (m,)
|
|
250
|
+
"""
|
|
251
|
+
return jnp.zeros(self.m)
|
cbfpy/envs/__init__.py
ADDED
|
File without changes
|