Lecture 23 - Control Barrier Functions

Published

April 8, 2026

Based on notes created by Sam Coogan and Murat Arcak. Licensed under a “Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License”

Control Barrier Functions

Consider a control-affine system

\[\dot{x} = f(x) + g(x)u \tag{1}\]

and a given set \(\mathcal{C} = \{x \mid h(x) \geq 0\}\). How can we choose a controller \(u(x)\) such that \(\mathcal{C}\) is positively invariant?

Recall Barrier Functions:

\[\dot{h}(x) = \nabla h(x)^Tf(x) \geq -\alpha(h(x)) \quad \text{for all } x\in\mathbb{R}^n\]

Recall that we also have the following theorem for barrier functions:

Theorem: Barrier Function

If \(h\) is a barrier function, then \(\mathcal{C}=\{x:h(x)\geq 0\}\) is positively invariant.

Definition: Control Barrier Function

A function \(h\) with \(\mathcal{C}=\{x\mid h(x)\geq 0\}\) is a control barrier function (CBF) for a control-affine system \(\dot{x} = f(x) + g(x)u\) if there exists a locally Lipschitz function \(\alpha:\mathbb{R}\to\mathbb{R}\) satisfying \(\alpha(0)=0\) such that

\[\sup_{u\in\mathbb{R}^m} \nabla h(x)^T(f(x)+g(x)u)\geq -\alpha (h(x))\quad \text{for all } x\in\mathbb{R}^n. \tag{2}\]

The supremum is the smallest number that is greater than or equal to every element in the set. The supremum must be a real number (cannot be infinity).

We can also write Equation 2 using Lie derivative notation:

\[\sup_{u\in\mathbb{R}^m} L_fh(x)+L_gh(x)u \geq -\alpha (h(x)) \tag{3}\]

Define

\[U(x)=\{u\in\mathbb{R}^m\mid \nabla h(x)^T(f(x)+g(x)u)\geq -\alpha (h(x))\}. \tag{4}\]

Theorem: Invariance from CBF

If \(h\) is a control barrier function for Equation 1, then the following hold:

  1. \(U(x)\neq \emptyset\) for all \(x\);
  2. Any Lipschitz feedback control \(u:\mathbb{R}^n\to \mathbb{R}^m\) satisfying \(u(x)\in U(x)\) renders \(\mathcal{C}\) invariant;
  3. A feedback control is given by \[u^*(x)= \begin{cases} 0 & \text{if }\nabla h(x)^T f(x)+\alpha(h(x))\geq 0\\ \dfrac{-\nabla h(x)^Tf(x)-\alpha(h(x))}{\|\nabla h(x)^T g(x)\|_2^2}(g(x)^T\nabla h(x)) & \text{otherwise,} \end{cases} \tag{5}\] which is the same as: \[u^*(x)= \begin{cases} 0 & \text{if } L_fh(x) +\alpha(h(x)) \geq 0\\ \dfrac{-(L_fh(x) + \alpha(h(x)))L_g h(x)^T}{L_gh(x) L_g h(x)^T} & \text{otherwise.} \end{cases}\] A sufficient condition for \(u^*(x)\) to be Lipschitz on some domain is that \(\nabla h(x)^Tg(x)\neq 0\) everywhere on the domain.

Proof:

  1. If \(\sup_{u\in\mathbb{R}^m} \nabla h(x)^T(f(x)+g(x)u)<\infty\), then the sup is attained for some \(u\).
  2. \(h\) becomes a (regular) barrier function for \(\tilde{f}(x)=f(x)+g(x)u(x)\) and the theorem from the previous lecture applies.
  3. (Sketch) First, note that \(u^*(x)\) is well-defined since \(\nabla h(x)^T g(x)\neq 0\) whenever \(\nabla h(x)^T f(x)+\alpha(h(x))< 0\) by the CBF condition. \(u^*(x)\) can be considered as a composition of 3 Lipschitz functions and is therefore Lipschitz. Finally, we can verify that \[\nabla h(x)^T(f(x)+g(x)u^*(x))+\alpha(h(x)) = \begin{cases} \nabla h(x)^Tf(x) +\alpha(h(x))&\text{if } \nabla h(x)^Tf(x) +\alpha(h(x))\geq 0\\ 0&\text{otherwise} \end{cases} \geq 0.\]

Minimum Effort Control

From the above proof, specifically the condition

\[\nabla h(x)^T(f(x)+g(x)u^*(x))+\alpha(h(x)) = \begin{cases} \nabla h(x)^Tf(x) +\alpha(h(x))&\text{if } \nabla h(x)^Tf(x) +\alpha(h(x))\geq 0\\ 0&\text{otherwise,} \end{cases}\]

we conclude that \(u^*(x)\) is the “minimum effort” controller, i.e., \(u^*(x)=\text{argmin}_{u\in U(x)}\|u\|_2^2\).

Example: Cart-Pole System

Recall the model of the cart-pole system from Lecture 16 (take \(m=M=\ell=1\)):

\[\begin{split} \ddot{y}=\dot{v} &= \frac{1}{1+ \sin^2 \theta} \Bigg( u + \dot \theta^2 \sin \theta - g \sin \theta \cos \theta \Bigg) \\ \ddot \theta &= \frac{1}{1 + \sin^2 \theta} \Bigg( -u \cos\theta - \dot \theta^2 \cos \theta \sin \theta + 2g \sin \theta \Bigg) \end{split}\]

where \(v=\dot{y}\) is velocity. Take as the state \(x=[y\ v\ \theta \ \dot{\theta}]^T\). Suppose we want \(v\) to satisfy \(-L\leq v\leq L.\) Choose

\[\begin{align} h(x) &= \frac{1}{2}(-v^2+L^2) \\ \alpha(s) &= \gamma s, \quad \gamma>0. \end{align}\]

Then

\[\begin{align} \nabla h(x)^Tf(x) &= L_fh(x) = \frac{-v}{1+\sin^2(\theta)}\left( \dot \theta^2 \sin \theta - g \sin \theta \cos \theta\right)\\ \nabla h(x)^Tg(x) &= L_gh(x) = \frac{-v}{1+\sin^2(\theta)}\\ \alpha(h(x)) &= \gamma h(x) \end{align}\]

and \(u^*(x)\) constructed as above.

The figures below show results for \(x_0=[y_0\ v_0\ \theta_0 \ \dot{\theta}_0]^T=[0\ 0\ \pi/2\ 0]^T\) using the \(u^*(x)\) from the theorem.

Show code
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

# Parameters (m = M = ell = 1)
g_gravity = 9.81
L_vel = 2.0  # velocity bound: |v| <= L

def h(x):
    v = x[1]
    return 0.5 * (-v**2 + L_vel**2)

def Lf_h(x):
    v, theta, thetadot = x[1], x[2], x[3]
    den = 1.0 + np.sin(theta)**2
    return (-v / den) * (thetadot**2 * np.sin(theta) - g_gravity * np.sin(theta) * np.cos(theta))

def Lg_h(x):
    v, theta = x[1], x[2]
    den = 1.0 + np.sin(theta)**2
    return -v / den

def cbf_controller(x, gamma):
    Lf = Lf_h(x)
    Lg = Lg_h(x)
    hx = h(x)
    cond = Lf + gamma * hx
    if cond >= 0.0:
        return 0.0
    return -cond * Lg / (Lg**2)

def make_ode(gamma):
    def cart_pole(t, x):
        y, v, theta, thetadot = x
        u = 0.0 if gamma is None else cbf_controller(x, gamma)
        den = 1.0 + np.sin(theta)**2
        vdot      = (u + thetadot**2 * np.sin(theta) - g_gravity * np.sin(theta) * np.cos(theta)) / den
        thetaddot = (-u * np.cos(theta) - thetadot**2 * np.cos(theta) * np.sin(theta) + 2 * g_gravity * np.sin(theta)) / den
        return [v, vdot, thetadot, thetaddot]
    return cart_pole

x0     = [0.0, 0.0, np.pi / 2, 0.0]
t_eval = np.linspace(0, 10, 2000)
opts   = dict(method='RK45', rtol=1e-8, atol=1e-10)

sol_none = solve_ivp(make_ode(None),  (0, 10), x0, t_eval=t_eval, **opts)
sol_g1   = solve_ivp(make_ode(1.0),   (0, 10), x0, t_eval=t_eval, **opts)
sol_g100 = solve_ivp(make_ode(100.0), (0, 10), x0, t_eval=t_eval, **opts)

def histories(sol, gamma):
    n = sol.y.shape[1]
    u = np.array([0.0 if gamma is None else cbf_controller(sol.y[:, i], gamma) for i in range(n)])
    hx = np.array([h(sol.y[:, i]) for i in range(n)])
    return u, hx

u_none, h_none   = histories(sol_none, None)
u_g1,   h_g1     = histories(sol_g1,   1.0)
u_g100, h_g100   = histories(sol_g100, 100.0)

# --- Figure: 3 stacked subplots ---
fig, axes = plt.subplots(3, 1, figsize=(8, 9), sharex=True)

colors = {'none': 'gray', 'g1': 'steelblue', 'g100': 'darkorange'}

# Subplot 1: cart velocity
ax = axes[0]
ax.plot(sol_none.t, sol_none.y[1], color=colors['none'],  lw=1.8, ls='--', label='No barrier')
ax.plot(sol_g1.t,   sol_g1.y[1],   color=colors['g1'],    lw=1.8,           label='CBF $\\gamma=1$')
ax.plot(sol_g100.t, sol_g100.y[1], color=colors['g100'],  lw=1.8,           label='CBF $\\gamma=100$')
ax.axhline( L_vel, color='red', ls=':', lw=1.4, label=f'$\\pm L={L_vel}$ m/s')
ax.axhline(-L_vel, color='red', ls=':', lw=1.4)
ax.set_ylabel('$v$ (m/s)')
ax.set_title('Cart Velocity')
ax.legend(loc='upper right')
ax.grid(True, alpha=0.4)

# Subplot 2: control input
ax = axes[1]
ax.plot(sol_none.t, u_none, color=colors['none'],  lw=1.8, ls='--', label='No barrier')
ax.plot(sol_g1.t,   u_g1,   color=colors['g1'],    lw=1.8,           label='CBF $\\gamma=1$')
ax.plot(sol_g100.t, u_g100, color=colors['g100'],  lw=1.8,           label='CBF $\\gamma=100$')
ax.set_ylabel('$u^*(t)$')
ax.set_title('Control Input')
ax.legend(loc='upper right')
ax.grid(True, alpha=0.4)

# Subplot 3: barrier function
ax = axes[2]
ax.plot(sol_none.t, h_none, color=colors['none'],  lw=1.8, ls='--', label='No barrier')
ax.plot(sol_g1.t,   h_g1,   color=colors['g1'],    lw=1.8,           label='CBF $\\gamma=1$')
ax.plot(sol_g100.t, h_g100, color=colors['g100'],  lw=1.8,           label='CBF $\\gamma=100$')
ax.axhline(0, color='black', ls='--', lw=1.2, label='$h=0$ (boundary)')
ax.fill_between(sol_g1.t, 0, np.minimum(h_g1, 0), alpha=0.15, color='red')
ax.set_ylabel('$h(x)$')
ax.set_xlabel('Time (s)')
ax.set_title('Barrier Function Value')
ax.legend(loc='upper right')
ax.grid(True, alpha=0.4)

plt.tight_layout()
plt.show()

The animation below shows the cart-pole trajectory for the uncontrolled system (no barrier) and the CBF controller with \(\gamma=100\).

Show code
from matplotlib import animation
from IPython.display import HTML

def make_cartpole_anim(sol, title='Cart-Pole', dt_frame=0.04):
    t      = sol.t
    y_cart = sol.y[0]
    theta  = sol.y[2]

    # Interpolate to fixed frame rate
    t_fr  = np.arange(t[0], t[-1], dt_frame)
    y_fr  = np.interp(t_fr, t, y_cart)
    th_fr = np.interp(t_fr, t, theta)

    x_lo = np.min(y_fr) - 1.8
    x_hi = np.max(y_fr) + 1.8

    fig, ax = plt.subplots(figsize=(9, 3.5))
    ax.set_xlim(x_lo, x_hi)
    ax.set_ylim(-1.5, 1.6)
    ax.set_aspect('equal')
    ax.set_title(title, fontsize=12)
    ax.axis('off')

    # Ground rail
    ax.axhline(0, color='#555', lw=2.5, zorder=1)
    ax.fill_between([x_lo, x_hi], -0.15, 0, color='#ccc', zorder=0)

    # Cart (rectangle centered at y, sitting on y=0)
    cw, ch = 0.5, 0.25
    cart = plt.Rectangle((y_fr[0] - cw/2, 0), cw, ch,
                          fc='steelblue', ec='#222', lw=1.5, zorder=3)
    ax.add_patch(cart)

    # Wheels
    w1 = plt.Circle((y_fr[0] - cw/4, 0), 0.07, fc='#222', zorder=4)
    w2 = plt.Circle((y_fr[0] + cw/4, 0), 0.07, fc='#222', zorder=4)
    ax.add_patch(w1)
    ax.add_patch(w2)

    # Pole: pivot at top-center of cart (y, ch), tip at (y+sin θ, ch+cos θ)
    pole_x0 = y_fr[0]
    pole_y0 = ch
    tip_x   = pole_x0 + np.sin(th_fr[0])
    tip_y   = pole_y0 + np.cos(th_fr[0])
    pole_line, = ax.plot([pole_x0, tip_x], [pole_y0, tip_y],
                         '-', color='#333', lw=4, solid_capstyle='round', zorder=5)
    bob = plt.Circle((tip_x, tip_y), 0.09, fc='#0077BE', ec='#222', lw=1.5, zorder=6)
    ax.add_patch(bob)

    time_txt = ax.text(0.02, 0.92, '', transform=ax.transAxes, fontsize=10)

    def init():
        _update(0)
        time_txt.set_text('')
        return cart, w1, w2, pole_line, bob, time_txt

    def _update(i):
        yi  = y_fr[i]
        thi = th_fr[i]
        cart.set_xy((yi - cw/2, 0))
        w1.center = (yi - cw/4, 0)
        w2.center = (yi + cw/4, 0)
        px0, py0 = yi, ch
        tx = px0 + np.sin(thi)
        ty = py0 + np.cos(thi)
        pole_line.set_data([px0, tx], [py0, ty])
        bob.center = (tx, ty)
        time_txt.set_text(f't = {t_fr[i]:.2f} s')
        return cart, w1, w2, pole_line, bob, time_txt

    def animate(i):
        return _update(i)

    anim = animation.FuncAnimation(fig, animate, init_func=init,
                                   frames=len(t_fr), interval=dt_frame * 1000,
                                   blit=True)
    plt.close(fig)
    return anim

No barrier (\(u=0\)): the cart accelerates freely and the velocity is unconstrained.

Show code
HTML(make_cartpole_anim(sol_none, title='Cart-Pole: No Barrier ($u=0$)').to_jshtml())

CBF controller (\(\gamma=100\)): the barrier function enforces \(|v| \leq L\) throughout the motion.

Show code
HTML(make_cartpole_anim(sol_g100, title='Cart-Pole: CBF Controller ($\\gamma=100$)').to_jshtml())

Controller Synthesis as Optimization Problem

For fixed \(x\), the CBF constraint is affine in \(u\). We can therefore define a convex program to compute a control input at each time instant:

\[\begin{aligned} u(x)=\;&\underset{\mu}{\text{arg min}} &&C(\mu,x)\\ &\text{subject to}&& \nabla h(x)^Tf(x)+\nabla h(x)^Tg(x)\mu\geq -\alpha (h(x)) \end{aligned} \tag{6}\]

where \(C(\mu,x)\) is some cost function that is convex in \(\mu\) for each fixed state \(x\).

Example 1: Suppose \(k(x):\mathbb{R}^n\to\mathbb{R}^m\) is some nominal feedback controller designed for some other purpose (e.g., performance objectives). Choose \(C(\mu,x)=\|\mu-k(x)\|_2^2\). The result is a quadratic program (with affine constraints) to compute \(u(x)\) at each \(x\).

  • Raises questions about solving a QP in real-time online; care must be taken with discretization values, etc.
  • Convex solvers are fast enough that they can be included “in-the-loop” and have been for applications like stable bipedal locomotion and quadrotor control.

Example 2: Consider our controlled pendulum:

\[\dot{x} = \frac{d}{dt}\begin{bmatrix} \theta \\ \dot{\theta} \end{bmatrix} = \begin{bmatrix} \dot{\theta} \\ -\frac{g}{l} \sin(\theta) + u \end{bmatrix} \quad \implies \quad \dot{x} = \begin{bmatrix} x_2 \\ -\frac{g}{l} \sin(x_1) + u \end{bmatrix}\]

Let’s assume that we want to limit the velocity of the pendulum. We can define our safe set as:

\[\mathcal{C} = \{x \mid h(x) := v_{\max}^2 - x_2^2 \geq 0 \}\]

Taking the derivative of \(h(x)\) we have:

\[\begin{align} \dot{h} &= -2 x_2 \dot{x}_2 = - 2 x_2 \left(-\frac{g}{l}\sin(x_1) + u\right) \\ &= \underbrace{2 x_2 \frac{g}{l}\sin(x_1)}_{L_f h} \underbrace{- 2 x_2}_{L_gh} u \end{align}\]

Thus, safety can be enforced via the CBF condition \(L_f h + L_g h u \geq -\alpha(h)\), where \(\alpha(h) = \gamma h\) for some \(\gamma > 0\).

Show code
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

# Parameters (m = ell = 1)
g_gravity = 9.81
L_vel = 2.0  # velocity bound: |v| <= L

def h(x):
    v = x[1]
    return 0.5 * (-v**2 + L_vel**2)

def Lf_h(x):
    theta, thetadot = x[0], x[1]
    return 2*thetadot * g_gravity * np.sin(theta)

def Lg_h(x):
    theta, thetadot = x[0], x[1]
    return -2*thetadot

def cbf_controller(x, gamma):
    Lf = Lf_h(x)
    Lg = Lg_h(x)
    hx = h(x)
    cond = Lf + gamma * hx
    if cond >= 0.0:
        return 0.0
    return -cond * Lg / (Lg**2)

def make_ode(gamma):
    def pendulum(t, x):
        theta, thetadot = x[0], x[1]
        u = 0.0 if gamma is None else cbf_controller(x, gamma)
        thetaddot = -g_gravity * np.sin(theta) + u
        return [thetadot, thetaddot]
    return pendulum

x0     = [np.pi / 2, 0.0]
t_eval = np.linspace(0, 10, 2000)
opts   = dict(method='RK45', rtol=1e-8, atol=1e-10)

sol_none = solve_ivp(make_ode(None),  (0, 10), x0, t_eval=t_eval, **opts)
sol_g1   = solve_ivp(make_ode(1.0),   (0, 10), x0, t_eval=t_eval, **opts)
sol_g100 = solve_ivp(make_ode(100.0), (0, 10), x0, t_eval=t_eval, **opts)

def histories(sol, gamma):
    n = sol.y.shape[1]
    u = np.array([0.0 if gamma is None else cbf_controller(sol.y[:, i], gamma) for i in range(n)])
    hx = np.array([h(sol.y[:, i]) for i in range(n)])
    return u, hx

u_none, h_none   = histories(sol_none, None)
u_g1,   h_g1     = histories(sol_g1,   1.0)
u_g100, h_g100   = histories(sol_g100, 100.0)

# --- Figure: 3 stacked subplots ---
fig, axes = plt.subplots(3, 1, figsize=(8, 9), sharex=True)

colors = {'none': 'gray', 'g1': 'steelblue', 'g100': 'darkorange'}

# Subplot 1: pendulum velocity
ax = axes[0]
ax.plot(sol_none.t, sol_none.y[1], color=colors['none'],  lw=1.8, ls='--', label='No barrier')
ax.plot(sol_g1.t,   sol_g1.y[1],   color=colors['g1'],    lw=1.8,           label='CBF $\\gamma=1$')
ax.plot(sol_g100.t, sol_g100.y[1], color=colors['g100'],  lw=1.8,           label='CBF $\\gamma=100$')
ax.axhline( L_vel, color='red', ls=':', lw=1.4, label=f'$\\pm L={L_vel}$ m/s')
ax.axhline(-L_vel, color='red', ls=':', lw=1.4)
ax.set_ylabel('$v$ (m/s)')
ax.set_title('Pendulum Angular Velocity')
ax.legend(loc='upper right')
ax.grid(True, alpha=0.4)

# Subplot 2: control input
ax = axes[1]
ax.plot(sol_none.t, u_none, color=colors['none'],  lw=1.8, ls='--', label='No barrier')
ax.plot(sol_g1.t,   u_g1,   color=colors['g1'],    lw=1.8,           label='CBF $\\gamma=1$')
ax.plot(sol_g100.t, u_g100, color=colors['g100'],  lw=1.8,           label='CBF $\\gamma=100$')
ax.set_ylabel('$u^*(t)$')
ax.set_title('Control Input')
ax.legend(loc='upper right')
ax.grid(True, alpha=0.4)

# Subplot 3: barrier function
ax = axes[2]
ax.plot(sol_none.t, h_none, color=colors['none'],  lw=1.8, ls='--', label='No barrier')
ax.plot(sol_g1.t,   h_g1,   color=colors['g1'],    lw=1.8,           label='CBF $\\gamma=1$')
ax.plot(sol_g100.t, h_g100, color=colors['g100'],  lw=1.8,           label='CBF $\\gamma=100$')
ax.axhline(0, color='black', ls='--', lw=1.2, label='$h=0$ (boundary)')
ax.fill_between(sol_g1.t, 0, np.minimum(h_g1, 0), alpha=0.15, color='red')
ax.set_ylabel('$h(x)$')
ax.set_xlabel('Time (s)')
ax.set_title('Barrier Function Value')
ax.legend(loc='upper right')
ax.grid(True, alpha=0.4)

plt.tight_layout()
plt.show()

Show code
from matplotlib import animation
from IPython.display import HTML

def make_pendulum_anim(sol, title='Pendulum', dt_frame=0.04, sol_ghost=None, figsize=(4, 4.5)):
    t     = sol.t
    theta = sol.y[0]

    t_fr  = np.arange(t[0], t[-1], dt_frame)
    th_fr = np.interp(t_fr, t, theta)

    # Pivot at origin, length = 1; theta=0 straight-down
    bx = np.sin(th_fr)
    by = -np.cos(th_fr)

    # Ghost (nominal) trajectory
    if sol_ghost is not None:
        th_ghost = np.interp(t_fr, sol_ghost.t, sol_ghost.y[0])
        bx_ghost = np.sin(th_ghost)
        by_ghost = -np.cos(th_ghost)

    fig, ax = plt.subplots(figsize=figsize)
    ax.set_xlim(-1.6, 1.6)
    ax.set_ylim(-1.5, 1.6)
    ax.set_aspect('equal')
    ax.axis('off')
    ax.set_title(title, fontsize=11)

    ax.plot(0, 0, 'ks', markersize=8, zorder=5)  # pivot

    # Ghost elements drawn behind the main pendulum
    if sol_ghost is not None:
        ghost_rod, = ax.plot([0, bx_ghost[0]], [0, by_ghost[0]], '-', color='#aaa', lw=3,
                             alpha=0.4, solid_capstyle='round', zorder=1)
        ghost_bob  = plt.Circle((bx_ghost[0], by_ghost[0]), 0.12,
                                fc='#aaa', ec='#777', lw=1.0, alpha=0.4, zorder=2)
        ax.add_patch(ghost_bob)

    rod,  = ax.plot([0, bx[0]], [0, by[0]], '-', color='#333', lw=3,
                    solid_capstyle='round', zorder=3)
    bob   = plt.Circle((bx[0], by[0]), 0.12, fc='#0077BE', ec='#222', lw=1.5, zorder=4)
    ax.add_patch(bob)

    trail_x, trail_y = [], []
    trail, = ax.plot([], [], '-', color='steelblue', alpha=0.3, lw=1.5, zorder=2)
    time_txt = ax.text(0.02, 0.96, '', transform=ax.transAxes, fontsize=9)

    artists = [rod, bob, trail, time_txt]
    if sol_ghost is not None:
        artists += [ghost_rod, ghost_bob]

    def init():
        trail_x.clear(); trail_y.clear()
        rod.set_data([0, bx[0]], [0, by[0]])
        bob.center = (bx[0], by[0])
        trail.set_data([], [])
        time_txt.set_text('')
        if sol_ghost is not None:
            ghost_rod.set_data([0, bx_ghost[0]], [0, by_ghost[0]])
            ghost_bob.center = (bx_ghost[0], by_ghost[0])
        return artists

    def animate(i):
        rod.set_data([0, bx[i]], [0, by[i]])
        bob.center = (bx[i], by[i])
        trail_x.append(bx[i]); trail_y.append(by[i])
        trail.set_data(trail_x[-60:], trail_y[-60:])
        time_txt.set_text(f't = {t_fr[i]:.2f} s')
        if sol_ghost is not None:
            ghost_rod.set_data([0, bx_ghost[i]], [0, by_ghost[i]])
            ghost_bob.center = (bx_ghost[i], by_ghost[i])
        return artists

    anim = animation.FuncAnimation(fig, animate, init_func=init,
                                   frames=len(t_fr), interval=dt_frame * 1000,
                                   blit=True)
    plt.close(fig)
    return anim

_strip_css = ('<style>[id$="_first_button"],[id$="_back_button"],'
             '[id$="_next_button"],[id$="_end_button"],'
             '[id$="_repeat_button"],[id$="_speed"]{display:none!important}</style>')

def _slim(anim):
    return _strip_css + anim.to_jshtml()

_fs = (2.5, 3.0)
_h1 = _slim(make_pendulum_anim(sol_none, title='No Barrier ($u=0$)',  figsize=_fs))
_h2 = _slim(make_pendulum_anim(sol_g1,   title='CBF $\\gamma=1$',    figsize=_fs))
_h3 = _slim(make_pendulum_anim(sol_g100, title='CBF $\\gamma=100$',  figsize=_fs))
HTML(f'<div style="display:flex;gap:6px;justify-content:center;overflow-x:auto;">'
     f'<div>{_h1}</div><div>{_h2}</div><div>{_h3}</div></div>')

Minimally Invasive CBF Controller

The CBF can be enforced alongside a nominal controller using a CBF-QP. For example, consider the pendulum with a sinusoid-tracking controller \(u_{des}\) obtained through input-output linearization. A minimally-invasive CBF-QP would be:

\[\begin{aligned} u^* = \underset{u}{\text{minimize}} \;&\|u - u_{des}\|_2^2 \\ \text{subject to} \;&L_fh +L_gh u + \gamma h \geq 0 \end{aligned}\]

Show code
import cvxpy as cp

def io_controller(x,t): # sinusoid tracking controller
    theta, thetadot = x[0], x[1]
    epsilon = 10
    period = 1.0
    xdes = np.deg2rad(45) * np.sin(2 * np.pi * t / period)  # Desired position in radians
    dxdes = np.deg2rad(45)*np.cos(2 * np.pi * t / period) * (2 * np.pi / period)  # Desired velocity in radians/sec
    v  = - (epsilon**2 * (theta-xdes)) - (2*epsilon * (thetadot-dxdes))
    return (g_gravity/L_vel)*np.sin(theta) + v

def cbf_tracking_controller(x, gamma, t):
    theta, thetadot = x[0], x[1]

    # Get desired input from io_controller
    u_des = io_controller(x, t)
    
    # Define CBF components 
    Lf = Lf_h(x)
    Lg = Lg_h(x)
    hx = h(x)

    if Lg == 0 and Lf + gamma * hx < 0:
        raise ValueError("Lg_h(x) * u cannot be zero. Adjust the control barrier function or system dynamics.")

    # Quadratic Program
    u = cp.Variable(1)
    cost = cp.quad_form(u - u_des, np.eye(1))
    
    cbf_constraint = Lf + Lg * u + gamma * hx >= 0
    
    prob = cp.Problem(cp.Minimize(cost), [cbf_constraint])
    prob.solve()

    # Use this control input
    u_safe = float(u.value[0])

    if prob.status != cp.OPTIMAL:
        print("Problem is not optimal Using default controller.")
        u_safe = u_des
    else:
        u_diff = u.value - u_des
        # print("Control input difference:", u_diff)
    
    return u_safe

def make_tracking_ode(gamma):
    def pendulum(t, x):
        theta, thetadot = x[0], x[1]
        u = io_controller(x,t) if gamma is None else cbf_tracking_controller(x, gamma, t)
        thetaddot = -g_gravity * np.sin(theta) + u
        return [thetadot, thetaddot]
    return pendulum

# Animation
sol_cbf     = solve_ivp(make_tracking_ode(100.0), (0, 10), x0, t_eval=t_eval, **opts)
sol_nominal = solve_ivp(make_tracking_ode(None),   (0, 10), x0, t_eval=t_eval, **opts)
HTML(make_pendulum_anim(sol_cbf, title='Pendulum Reference Tracking ($\\gamma=100$)',
                        sol_ghost=sol_nominal).to_jshtml())

Need for Higher-Order CBFs

For CBFs to be implemented as previously discussed, we need to ensure that \(\nabla h(x)^Tg(x) = L_g h(x) \neq 0\) for all \(x\) in the domain of interest. However, this would preclude us from selecting a CBF to limit the pendulum position, i.e., \(h(x) := \theta_{\max}^2 - x_1^2\). In this case, we need higher-order CBFs. We will cover these in the next lecture.