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,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