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/cbfs/clf_cbf.py ADDED
@@ -0,0 +1,490 @@
1
+ """
2
+ # Control Lyapunov Function / Control Barrier Functions (CLF-CBFs)
3
+
4
+ Whereas a CBF acts as a safety filter on top of a nominal controller, a CLF-CBF acts as a safe controller itself,
5
+ based on a control objective defined by the CLF and a safety constraint defined by the CBF. Note that the CLF
6
+ objective should be quadratic and positive-definite to fit in this QP framework.
7
+
8
+ The CLF-CBF optimizes the following:
9
+ ```
10
+ minimize ||u||_{2}^{2} # CLF Objective (Example)
11
+ subject to LfV(z) + LgV(z)u <= -gamma(V(z)) # CLF Constraint
12
+ Lfh(z) + Lgh(z)u >= -alpha(h(z)) # CBF Constraint
13
+ ```
14
+
15
+ As with the CBF, if this is a relative-degree-2 system, we update the constraints:
16
+ ```
17
+ minimize ||u||_{2}^{2} # CLF Objective (Example)
18
+ subject to LfV_2(z) + LgV_2(z)u <= -gamma_2(V_2(z)) # RD2 CLF Constraint
19
+ Lfh_2(z) + Lgh_2(z)u >= -alpha_2(h_2(z)) # RD2 CBF Constraint
20
+ ```
21
+
22
+ If there are constraints on the control input, we also enforce another constraint:
23
+ ```
24
+ u_min <= u <= u_max # Control constraint
25
+ ```
26
+
27
+ However, in general the CLF constraint and the CBF constraint cannot be strictly enforced together. We then
28
+ need to introduce a slack variable to relax the CLF constraint, ensuring that the CBF safety condition takes
29
+ priority over the CLF objective.
30
+
31
+ The optimization problem then becomes:
32
+ ```
33
+ minimize ||u||_{2}^{2} + p * delta^2 # CLF Objective (Example)
34
+ subject to LfV(z) + LgV(z)u <= -gamma(V(z)) + delta # CLF Constraint
35
+ Lfh(z) + Lgh(z)u >= -alpha(h(z)) # CBF Constraint
36
+ ```
37
+ where `p` is a large constant and `delta` is the slack variable.
38
+ """
39
+
40
+ from typing import Tuple, Callable, Optional
41
+
42
+ import jax
43
+ import jax.numpy as jnp
44
+ from jax import Array
45
+ from jax.typing import ArrayLike
46
+ import qpax
47
+
48
+ from cbfpy.config.clf_cbf_config import CLFCBFConfig
49
+ from cbfpy.utils.jax_utils import conditional_jit
50
+ from cbfpy.utils.general_utils import print_warning
51
+
52
+ # Debugging flags to disable jit in specific sections of the code.
53
+ # Note: If any higher-level jits exist, those must also be set to debug (disable jit)
54
+ DEBUG_CONTROLLER = False
55
+ DEBUG_QP_DATA = False
56
+
57
+
58
+ @jax.tree_util.register_static
59
+ class CLFCBF:
60
+ """Control Lyapunov Function / Control Barrier Function (CLF-CBF) class.
61
+
62
+ The main constructor for this class is via the `from_config` method, which constructs a CLF-CBF instance
63
+ based on the provided CLFCBFConfig configuration object.
64
+
65
+ You can then use the CLF-CBF's `controller` method to compute the optimal control input
66
+
67
+ Examples:
68
+ ```
69
+ # Construct a CLFCBFConfig for your problem
70
+ config = DroneConfig()
71
+ # Construct a CBF instance based on the config
72
+ clf_cbf = CLFCBF.from_config(config)
73
+ # Compute the safe control input
74
+ safe_control = clf_cbf.controller(current_state, desired_state)
75
+ ```
76
+ """
77
+
78
+ # NOTE: The __init__ method is not used to construct a CLFCBF instance. Instead, use the `from_config` method.
79
+ # This is because Jax prefers for the __init__ method to not contain any input validation, so we do this
80
+ # in the CLFCBFConfig class instead.
81
+ def __init__(
82
+ self,
83
+ n: int,
84
+ m: int,
85
+ num_cbf: int,
86
+ num_clf: int,
87
+ u_min: Optional[tuple],
88
+ u_max: Optional[tuple],
89
+ control_constrained: bool,
90
+ relax_cbf: bool,
91
+ cbf_relaxation_penalty: float,
92
+ clf_relaxation_penalty: float,
93
+ h_1: Callable[[ArrayLike], Array],
94
+ h_2: Callable[[ArrayLike], Array],
95
+ f: Callable[[ArrayLike], Array],
96
+ g: Callable[[ArrayLike], Array],
97
+ alpha: Callable[[ArrayLike], Array],
98
+ alpha_2: Callable[[ArrayLike], Array],
99
+ V_1: Callable[[ArrayLike], Array],
100
+ V_2: Callable[[ArrayLike], Array],
101
+ gamma: Callable[[ArrayLike], Array],
102
+ gamma_2: Callable[[ArrayLike], Array],
103
+ H: Callable[[ArrayLike], Array],
104
+ F: Callable[[ArrayLike], Array],
105
+ solver_tol: float,
106
+ ):
107
+ self.n = n
108
+ self.m = m
109
+ self.num_cbf = num_cbf
110
+ self.num_clf = num_clf
111
+ self.u_min = u_min
112
+ self.u_max = u_max
113
+ self.control_constrained = control_constrained
114
+ self.relax_cbf = relax_cbf
115
+ self.cbf_relaxation_penalty = cbf_relaxation_penalty
116
+ self.clf_relaxation_penalty = clf_relaxation_penalty
117
+ self.h_1 = h_1
118
+ self.h_2 = h_2
119
+ self.f = f
120
+ self.g = g
121
+ self.alpha = alpha
122
+ self.alpha_2 = alpha_2
123
+ self.V_1 = V_1
124
+ self.V_2 = V_2
125
+ self.gamma = gamma
126
+ self.gamma_2 = gamma_2
127
+ self.H = H
128
+ self.F = F
129
+ self.solver_tol = solver_tol
130
+ if relax_cbf:
131
+ self.qp_solver: Callable = jax.jit(qpax.solve_qp_elastic)
132
+ else:
133
+ self.qp_solver: Callable = jax.jit(qpax.solve_qp)
134
+
135
+ @classmethod
136
+ def from_config(cls, config: CLFCBFConfig) -> "CLFCBF":
137
+ """Construct a CLF-CBF based on the provided configuration
138
+
139
+ Args:
140
+ config (CLFCBFConfig): Config object for the CLF-CBF. Contains info on the system dynamics, barrier
141
+ function, Lyapunov function, etc.
142
+
143
+ Returns:
144
+ CLFCBF: Control Lyapunov Function / Control Barrier Function instance
145
+ """
146
+ instance = cls(
147
+ config.n,
148
+ config.m,
149
+ config.num_cbf,
150
+ config.num_clf,
151
+ config.u_min,
152
+ config.u_max,
153
+ config.control_constrained,
154
+ config.relax_cbf,
155
+ config.cbf_relaxation_penalty,
156
+ config.clf_relaxation_penalty,
157
+ config.h_1,
158
+ config.h_2,
159
+ config.f,
160
+ config.g,
161
+ config.alpha,
162
+ config.alpha_2,
163
+ config.V_1,
164
+ config.V_2,
165
+ config.gamma,
166
+ config.gamma_2,
167
+ config.H,
168
+ config.F,
169
+ config.solver_tol,
170
+ )
171
+ instance._validate_instance(*config.init_args)
172
+ return instance
173
+
174
+ def _validate_instance(self, *h_args) -> None:
175
+ """Checks that the CLF-CBF is valid; warns the user if not
176
+
177
+ Args:
178
+ *h_args: Optional additional arguments for the barrier function.
179
+ """
180
+ test_z = jnp.ones(self.n)
181
+ try:
182
+ test_lgh = self.Lgh(test_z, *h_args)
183
+ if jnp.allclose(test_lgh, 0):
184
+ print_warning(
185
+ "Lgh is zero. Consider increasing the relative degree or modifying the barrier function."
186
+ )
187
+ except TypeError:
188
+ print_warning(
189
+ "Cannot test Lgh; missing additional arguments.\n"
190
+ + "Please provide an initial seed for these args in the config's init_args input"
191
+ )
192
+ test_lgv = self.LgV(test_z)
193
+ if jnp.allclose(test_lgv, 0):
194
+ print_warning(
195
+ "LgV is zero. Consider increasing the relative degree or modifying the Lyapunov function."
196
+ )
197
+
198
+ @conditional_jit(not DEBUG_CONTROLLER)
199
+ def controller(self, z: Array, z_des: Array, *h_args) -> Array:
200
+ """Compute the CLF-CBF optimal control input, optimizing for the CLF objective while
201
+ satisfying the CBF safety constraint.
202
+
203
+ Args:
204
+ z (Array): State, shape (n,)
205
+ z_des (Array): Desired state, shape (n,)
206
+ *h_args: Optional additional arguments for the barrier function.
207
+
208
+ Returns:
209
+ Array: Safe control input, shape (m,)
210
+ """
211
+ P, q, A, b, G, h = self.qp_data(z, z_des, *h_args)
212
+ if self.relax_cbf:
213
+ x_qp, t_qp, s1_qp, s2_qp, z1_qp, z2_qp, converged, iters = self.qp_solver(
214
+ P,
215
+ q,
216
+ G,
217
+ h,
218
+ self.cbf_relaxation_penalty,
219
+ solver_tol=self.solver_tol,
220
+ )
221
+ else:
222
+ x_qp, s_qp, z_qp, y_qp, converged, iters = self.qp_solver(
223
+ P,
224
+ q,
225
+ A,
226
+ b,
227
+ G,
228
+ h,
229
+ solver_tol=self.solver_tol,
230
+ )
231
+ if DEBUG_CONTROLLER:
232
+ print(
233
+ f"{'Converged' if converged else 'Did not converge'}. Iterations: {iters}"
234
+ )
235
+ return x_qp[: self.m]
236
+
237
+ def h(self, z: ArrayLike, *h_args) -> Array:
238
+ """Barrier function(s)
239
+
240
+ Args:
241
+ z (ArrayLike): State, shape (n,)
242
+ *h_args: Optional additional arguments for the barrier function.
243
+
244
+ Returns:
245
+ Array: Barrier function evaluation, shape (num_barr,)
246
+ """
247
+
248
+ # Take any relative-degree-2 barrier functions and convert them to relative-degree-1
249
+ def _h_2(state):
250
+ return self.h_2(state, *h_args)
251
+
252
+ h_2, dh_2_dt = jax.jvp(_h_2, (z,), (self.f(z),))
253
+ h_2_as_rd1 = dh_2_dt + self.alpha_2(h_2)
254
+
255
+ # Merge the relative-degree-1 and relative-degree-2 barrier functions
256
+ return jnp.concatenate([self.h_1(z, *h_args), h_2_as_rd1])
257
+
258
+ def h_and_Lfh( # pylint: disable=invalid-name
259
+ self, z: ArrayLike, *h_args
260
+ ) -> Tuple[Array, Array]:
261
+ """Lie derivative of the barrier function(s) wrt the autonomous dynamics `f(z)`
262
+
263
+ The evaluation of the barrier function is also returned "for free", a byproduct of the jacobian-vector-product
264
+
265
+ Args:
266
+ z (ArrayLike): State, shape (n,)
267
+ *h_args: Optional additional arguments for the barrier function.
268
+
269
+ Returns:
270
+ h (Array): Barrier function evaluation, shape (num_barr,)
271
+ Lfh (Array): Lie derivative of `h` w.r.t. `f`, shape (num_barr,)
272
+ """
273
+ # Note: the below code is just a more efficient way of stating `Lfh = jax.jacobian(self.h)(z) @ self.f(z)`
274
+ # with the bonus benefit of also evaluating the barrier function
275
+
276
+ def _h(state):
277
+ return self.h(state, *h_args)
278
+
279
+ return jax.jvp(_h, (z,), (self.f(z),))
280
+
281
+ def Lgh(self, z: ArrayLike, *h_args) -> Array: # pylint: disable=invalid-name
282
+ """Lie derivative of the barrier function(s) wrt the control dynamics `g(z)u`
283
+
284
+ Args:
285
+ z (ArrayLike): State, shape (n,)
286
+ *h_args: Optional additional arguments for the barrier function.
287
+
288
+ Returns:
289
+ Array: Lgh, shape (num_barr, m)
290
+ """
291
+ # Note: the below code is just a more efficient way of stating `Lgh = jax.jacobian(self.h)(z) @ self.g(z)`
292
+
293
+ def _h(state):
294
+ return self.h(state, *h_args)
295
+
296
+ def _jvp(g_column):
297
+ return jax.jvp(_h, (z,), (g_column,))[1]
298
+
299
+ return jax.vmap(_jvp, in_axes=1, out_axes=1)(self.g(z))
300
+
301
+ ## CLF functions ##
302
+
303
+ def V(self, z: ArrayLike) -> Array:
304
+ """Control Lyapunov Function(s)
305
+
306
+ Args:
307
+ z (ArrayLike): State, shape (n,)
308
+
309
+ Returns:
310
+ Array: CLF evaluation, shape (num_clf,)
311
+ """
312
+ # Take any relative-degree-2 CLFs and convert them to relative-degree-1
313
+ # NOTE: If adding args to the CLF, create a wrapper func like with the barrier function
314
+ V_2, dV_2_dt = jax.jvp(self.V_2, (z,), (self.f(z),))
315
+ V2_rd1 = dV_2_dt + self.gamma_2(V_2)
316
+
317
+ # Merge the relative-degree-1 and relative-degree-2 CLFs
318
+ return jnp.concatenate([self.V_1(z), V2_rd1])
319
+
320
+ def V_and_LfV(self, z: ArrayLike) -> Tuple[Array, Array]:
321
+ """Lie derivative of the CLF wrt the autonomous dynamics `f(z)`
322
+
323
+ The evaluation of the CLF is also returned "for free", a byproduct of the jacobian-vector-product
324
+
325
+ Args:
326
+ z (ArrayLike): State, shape (n,)
327
+
328
+ Returns:
329
+ V (Array): CLF evaluation, shape (1,)
330
+ LfV (Array): Lie derivative of `V` w.r.t. `f`, shape (1,)
331
+ """
332
+ return jax.jvp(self.V, (z,), (self.f(z),))
333
+
334
+ def LgV(self, z: ArrayLike) -> Array:
335
+ """Lie derivative of the CLF wrt the control dynamics `g(z)u`
336
+
337
+ Args:
338
+ z (ArrayLike): State, shape (n,)
339
+
340
+ Returns:
341
+ Array: LgV, shape (m,)
342
+ """
343
+
344
+ def _jvp(g_column):
345
+ return jax.jvp(self.V, (z,), (g_column,))[1]
346
+
347
+ return jax.vmap(_jvp, in_axes=1, out_axes=1)(self.g(z))
348
+
349
+ ## QP Matrices ##
350
+
351
+ def P_qp( # pylint: disable=invalid-name
352
+ self, z: Array, z_des: Array, *h_args
353
+ ) -> Array:
354
+ """Quadratic term in the QP objective (`minimize 0.5 * x^T P x + q^T x`)
355
+
356
+ Args:
357
+ z (Array): State, shape (n,)
358
+ z_des (Array): Desired state, shape (n,)
359
+ *h_args: Optional additional arguments for the barrier function.
360
+
361
+ Returns:
362
+ Array: P matrix, shape (m, m)
363
+ """
364
+ return jnp.block(
365
+ [
366
+ [self.H(z), jnp.zeros((self.m, 1))],
367
+ [jnp.zeros((1, self.m)), jnp.atleast_1d(self.clf_relaxation_penalty)],
368
+ ]
369
+ )
370
+
371
+ def q_qp(self, z: Array, z_des: Array, *h_args) -> Array:
372
+ """Linear term in the QP objective (`minimize 0.5 * x^T P x + q^T x`)
373
+
374
+ Args:
375
+ z (Array): State, shape (n,)
376
+ z_des (Array): Desired state, shape (n,)
377
+ *h_args: Optional additional arguments for the barrier function.
378
+
379
+ Returns:
380
+ Array: Q vector, shape (m,)
381
+ """
382
+ return jnp.concatenate([self.F(z), jnp.array([0.0])])
383
+
384
+ def G_qp( # pylint: disable=invalid-name
385
+ self, z: Array, z_des: Array, *h_args
386
+ ) -> Array:
387
+ """Inequality constraint matrix for the QP (`Gx <= h`)
388
+
389
+ Note:
390
+ The number of constraints depends on if we have control constraints or not.
391
+ Without control constraints, `num_constraints == num_barriers`.
392
+ With control constraints, `num_constraints == num_barriers + 2*m`
393
+
394
+ Args:
395
+ z (Array): State, shape (n,)
396
+ z_des (Array): Desired state, shape (n,)
397
+ *h_args: Optional additional arguments for the barrier function.
398
+
399
+ Returns:
400
+ Array: G matrix, shape (num_constraints, m)
401
+ """
402
+ G = jnp.block(
403
+ [
404
+ [self.LgV(z), -1.0 * jnp.ones((self.num_clf, 1))],
405
+ [-self.Lgh(z, *h_args), jnp.zeros((self.num_cbf, 1))],
406
+ ]
407
+ )
408
+ if self.control_constrained:
409
+ return jnp.block(
410
+ [
411
+ [G],
412
+ [jnp.eye(self.m), jnp.zeros((self.m, 1))],
413
+ [-jnp.eye(self.m), jnp.zeros((self.m, 1))],
414
+ ]
415
+ )
416
+ else:
417
+ return G
418
+
419
+ def h_qp(self, z: Array, z_des: Array, *h_args) -> Array:
420
+ """Upper bound on constraints for the QP (`Gx <= h`)
421
+
422
+ Note:
423
+ The number of constraints depends on if we have control constraints or not.
424
+ Without control constraints, `num_constraints == num_barriers`.
425
+ With control constraints, `num_constraints == num_barriers + 2*m`
426
+
427
+ Args:
428
+ z (Array): State, shape (n,)
429
+ z_des (Array): Desired state, shape (n,)
430
+ *h_args: Optional additional arguments for the barrier function.
431
+
432
+ Returns:
433
+ Array: h vector, shape (num_constraints,)
434
+ """
435
+ hz, lfh = self.h_and_Lfh(z, *h_args)
436
+ vz, lfv = self.V_and_LfV(z)
437
+ h = jnp.concatenate(
438
+ [
439
+ -lfv - self.gamma(vz),
440
+ self.alpha(hz) + lfh,
441
+ ]
442
+ )
443
+ if self.control_constrained:
444
+ return jnp.concatenate(
445
+ [h, jnp.asarray(self.u_max), -jnp.asarray(self.u_min)]
446
+ )
447
+ else:
448
+ return h
449
+
450
+ @conditional_jit(not DEBUG_QP_DATA)
451
+ def qp_data(
452
+ self, z: Array, z_des: Array, *h_args
453
+ ) -> Tuple[Array, Array, Array, Array, Array, Array]:
454
+ """Constructs the QP matrices based on the current state and desired control
455
+
456
+ i.e. the matrices/vectors (P, q, A, b, G, h) for the optimization problem:
457
+
458
+ ```
459
+ minimize 0.5 * x^T P x + q^T x
460
+ subject to A x == b
461
+ G x <= h
462
+ ```
463
+
464
+ Note:
465
+ - CBFs do not rely on equality constraints, so `A` and `b` are empty.
466
+ - The number of constraints depends on if we have control constraints or not.
467
+ Without control constraints, `num_constraints == num_barriers`.
468
+ With control constraints, `num_constraints == num_barriers + 2*m`
469
+
470
+ Args:
471
+ z (Array): State, shape (n,)
472
+ z_des (Array): Desired state, shape (n,)
473
+ *h_args: Optional additional arguments for the barrier function.
474
+
475
+ Returns:
476
+ P (Array): Quadratic term in the QP objective, shape (m + 1, m + 1)
477
+ q (Array): Linear term in the QP objective, shape (m + 1,)
478
+ A (Array): Equality constraint matrix, shape (0, m + 1)
479
+ b (Array): Equality constraint vector, shape (0,)
480
+ G (Array): Inequality constraint matrix, shape (num_constraints, m + 1)
481
+ h (Array): Upper bound on constraints, shape (num_constraints,)
482
+ """
483
+ return (
484
+ self.P_qp(z, z_des, *h_args),
485
+ self.q_qp(z, z_des, *h_args),
486
+ jnp.zeros((0, self.m + 1)), # Equality matrix (not used for CLF-CBF)
487
+ jnp.zeros(0), # Equality vector (not used for CLF-CBF)
488
+ self.G_qp(z, z_des, *h_args),
489
+ self.h_qp(z, z_des, *h_args),
490
+ )
File without changes