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.
@@ -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
- 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.
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 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
+ 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
- relax_cbf: bool = True,
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(relax_cbf, bool):
99
- raise ValueError(f"relax_cbf must be a boolean. Got: {relax_cbf}")
100
- self.relax_cbf = relax_cbf
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 not isinstance(init_args, tuple):
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 = 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)
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 = jnp.concatenate([h1_test, h2_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(self.alpha, self.num_cbf)
185
- self._check_class_kappa(self.alpha_2, self.num_rd2_cbf)
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, *h_args) -> Array:
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, *h_args) -> Array:
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, *h_args) -> 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, *h_args) -> 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 jnp.allclose(func(jnp.zeros(dim)), 0.0)
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 = jnp.repeat(
370
- jnp.linspace(-1e6, 1e6, n_test).reshape(n_test, 1), dim, axis=1
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(func, in_axes=0)(test_points)
373
- assert jnp.all(a[:-1, :] < a[1:, :])
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"
@@ -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
- 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.
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 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 ().
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
- relax_cbf: bool = True,
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
- relax_cbf,
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 = jnp.ones(self.n)
113
- v1_test = self.V_1(jnp.ones(self.n))
114
- v2_test = self.V_2(jnp.ones(self.n))
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 = jnp.concatenate([v1_test, v2_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(self.gamma, self.num_clf)
139
- self._check_class_kappa(self.gamma_2, self.num_rd2_clf)
140
- H_test = self.H(z_test)
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
- def V_1(self, z: ArrayLike) -> Array:
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
@@ -1,5 +1,5 @@
1
1
  """
2
- # Drone Environment
2
+ # Drone Environment
3
3
 
4
4
  This is a wrapper around the gym-pybullet-drones environment, using velocity control.
5
5
  """
@@ -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
- relax_cbf=False,
53
- # If indeed relaxing, ensure that the QP relaxation >> the CLF relaxation
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=1e6,
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
 
@@ -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
- relax_cbf=True,
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([self.pos_max - z[:3], z[:3] - self.pos_min])
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