Safety Filters and Control Barrier Functions

Safety filters are control techniques that modify nominal control inputs to ensure system safety while minimizing deviation from desired behavior. They are based on Control Barrier Functions (CBF) and provide formal safety guarantees through quadratic programming (QP) optimization.

Theory

Control Barrier Functions

Consider a nonlinear control-affine system:

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

where $x \in \mathbb{R}^{n_x}$ is the state and $u \in \mathbb{R}^{n_u}$ is the control input.

A Control Barrier Function (CBF) $h(x)$ defines a safe set $\mathcal{C} = \{x : h(x) \geq 0\}$. The goal is to ensure the system remains in this safe set for all time. For systems with relative degree 1 (i.e., the input appears directly in the derivative of $h$), the safety condition is:

\[\dot{h}(x) = L_f h(x) + L_g h(x) u \geq -\alpha(h(x))\]

where $L_f h$ and $L_g h$ are Lie derivatives, and $\alpha$ is a class-$\mathcal{K}$ function (typically $\alpha(h) = h$ for simplicity).

Safety Filter Design

The safety filter solves the following quadratic program at each time step:

\[\begin{aligned} u^* = \arg\min_u \quad & \|u - u_{\text{nom}}\|^2 \\ \text{subject to} \quad & L_f h(x) + L_g h(x) u \geq -\alpha(h(x)) \\ & u_{\min} \leq u \leq u_{\max} \end{aligned}\]

This formulation finds the control input closest to the nominal control $u_{\text{nom}}$ while ensuring safety constraints are satisfied.

Exponential Control Barrier Functions

For systems with relative degree $\nu > 1$, standard CBFs cannot be directly applied. Exponential Control Barrier Functions (ECBF) extend CBFs to handle higher relative degrees by introducing a coordinate transformation:

\[z = T(x) = \begin{bmatrix} h(x) \\ L_f h(x) \\ \vdots \\ L_f^{\nu-1} h(x) \end{bmatrix}\]

The ECBF constraint becomes:

\[L_f^\nu h(x) + L_g L_f^{\nu-1} h(x) u \geq -K_\alpha z\]

where $K_\alpha$ is a gain vector chosen to ensure that $h$ decreases no faster than an exponentially decaying system with specified poles. This can be designed using pole placement techniques.

For more details on the theoretical foundations, see Ames et al., "Control Barrier Functions: Theory and Applications".

Connection between exponential CBFs and feedback linearization

The condition relating the decay of the barrier function to the pole placement of a linear system is closely related to feedback linearization. We can view this condition as making sure that the barrier function $h$ does not decay faster than the corresponding feedback linearizing controller can take over to keep it positive. If we place the poles of this linearly decaying system too far to the left, we allow ourselves to get closer to the barrier boundary before the safety filter intervenes, but when it does intervene, it will do so more aggressively. Note, the safety filter cannot account for potential future violations of actuator constraints, so an aggressive tuning may yield an infeasible QP if the nominal controller is heading towards the barrier boundary too fast. If actuator constraints become problematic, consider using, e.g., an MPC controller that natively considers future constraints, or design a barrier function that encodes a recovery procedure and use a safety filter to ensure that the system can always execute this recovery procedure. While such a recovery barrier function involves online future prediction much like MPC, it is typically less computationally costly since it does not perform any optimization over the future trajectory.

Implementation in DyadControlSystems

DyadControlSystems provides the following functions for safety filter design:

  • safetyfilter: Creates a basic safety filter for systems with relative degree 1
  • exponential_safetyfilter: Creates an exponential safety filter for systems with arbitrary relative degree
  • simulate: Simulates a system with safety filter and nominal controller
  • SafetyFilterSolution: Solution structure with automatic plotting support

Both filter types return callable structs that can be used as u_safe = filter(x, u_nom, p, t).

Examples

Example 1: Obstacle Avoidance

This example demonstrates a basic safety filter for obstacle avoidance in 2D space. The system is a double integrator where we directly control velocity, and we want to avoid a circular obstacle while reaching the origin.

using DyadControlSystems
using Symbolics
using LinearAlgebra
using Plots

# Define symbolic variables
@variables x1 x2 u[1:2]
x = [x1; x2]
u = collect(u)

# Simple dynamics: ẋ = u (direct velocity control)
f = [0; 0]
g = diagm([1, 1])

# Obstacle parameters
xo = [-1.0, 1.0]  # Obstacle center
ro = 0.4          # Obstacle radius

# Barrier function: h(x) = ||x - xo||² - ro² ≥ 0 keeps us outside the circle
h = norm(x - xo)^2 - ro^2

# Create safety filter
umin = -1
umax = 1
filter = safetyfilter(f, g, h, x; umin, umax)

# Nominal controller: simple proportional control to origin
u_nom_func(x, p, t) = -x

# Simulate
x0 = [-2.1, 2.0]  # Start position
sol = simulate(filter, x0, u_nom_func, (0.0, 15.0))

# Plot results
p1 = plot(sol.x[1,:], sol.x[2,:],
    label="Trajectory", lw=2,
    title="Obstacle Avoidance with Safety Filter",
    xlabel="x₁", ylabel="x₂", aspect_ratio=:equal)
scatter!([xo[1]], [xo[2]], label="Obstacle center", ms=8, color=:red)
θ = range(0, 2π, length=100)
plot!(xo[1] .+ ro*cos.(θ), xo[2] .+ ro*sin.(θ),
    label="Obstacle boundary", color=:red, lw=2)
scatter!([x0[1]], [x0[2]], label="Start", ms=6, color=:green)
scatter!([0], [0], label="Goal", ms=6, color=:blue)

p2 = plot(sol, ploth=true, plotu=true, plotx=false, plotactive=true, layout=4)
plot(p1, p2, layout=(1,2), size=(1200, 400))
Example block output

The trajectory successfully avoids the obstacle while reaching the origin. The safety filter intervenes when necessary to maintain the barrier constraint $h(x) \geq 0$.

Example 2: Exponential Safety Filter with Feedback Linearization

This example combines an exponential safety filter with feedback linearization for a third-order nonlinear system. The system has relative degree 3, requiring the exponential CBF formulation.

using DyadControlSystems
using Symbolics
using Plots

@variables x[1:3] u
x = collect(x)

# Nonlinear system with relative degree 3
f = [
    x[2]
    sin(x[3])
    0
]
g = [0; 0; 1]
h_output = x[1]  # Output for feedback linearization

# First, design a feedback linearizing controller
fl_result = feedbacklin(f, g, h_output, x)
fl_sys = Symbolics.build_function(fl_result; simplify=false)

# Reference tracking controller in linearized coordinates
v_func = function (x, p, t)
    y, yd, ydd = fl_sys.T(x, p, t)  # Transform to linear coordinates
    ref = 1.0  # Step reference
    e = ref - y
    ed = 0 - yd
    edd = 0 - ydd
    5*(e + 1*ed + 1*edd)  # PDD² controller
end

# Safety constraint: keep x[1] < 1.1
h_safety = 1.1 - x[1]

filter = exponential_safetyfilter(f, g, h_safety, x;
    umin=-10, umax=10,
    desired_poles=[-1.0, -1.5, -2.0])

# Nominal controller function (transforms x -> v -> u)
u_nom_fun = function(x, p, t)
    v = v_func(x, p, t)
    fl_sys.u(x, v, p, t)
end

# Simulate with safety filter
x0 = [0.5, 0.0, π/4]
sol_safe = DyadControlSystems.simulate(filter, x0, u_nom_fun, (0, 15))

# Also simulate without safety filter for comparison
sol_unsafe = DyadControlSystems.simulate(fl_sys, x0, v_func, (0, 15))

# Plot comparison
plot(sol_unsafe.t, sol_unsafe.x[1,:], label="Without safety filter", lw=2)
plot!(sol_safe.t, sol_safe.x[1,:], label="With safety filter", lw=2)
hline!([1.0], label="Reference", ls=:dash, color=:green)
hline!([1.1], label="Safety limit", ls=:dash, color=:red)
xlabel!("Time (s)")
ylabel!("x₁")
title!("Exponential Safety Filter with Feedback Linearization")
Example block output

The exponential safety filter successfully enforces the safety constraint $x_1 < 1.1$ while allowing the system to track the reference as closely as possible.

Example 3: Safety Filter with ModelingToolkit

Safety filters can also be applied to systems defined using ModelingToolkit, enabling physical modeling with safety guarantees. This example models and simulates a simple pendulum with a safety constraint on the angle.

using DyadControlSystems, ModelingToolkit
using ModelingToolkit: t_nounits as t, D_nounits as D
using StaticArrays, Plots

# Define a simple pendulum system
@mtkmodel Pendulum begin
    @parameters begin
        m = 1.0
        l = 1.0
        g = 9.81
        b = 0.1  # Damping
    end
    @variables begin
        θ(t) = -π/6     # Angle
        ω(t) = 0.0     # Angular velocity
        τ(t)           # Control torque
        h1(t)           # Barrier function
        h2(t)           # Barrier function
    end
    @equations begin
        D(θ) ~ ω
        D(ω) ~ (-g/l*sin(θ) - b*ω + τ)/(m*l^2)
        h1 ~ deg2rad(5) - θ  # Safety: keep angle smaller than +5 degrees
        h2 ~ deg2rad(5) + θ  # Safety: keep angle larger than -5 degrees
    end
end

@named pend = Pendulum()
model = complete(pend)

# Create safety filter directly from ModelingToolkit model
inputs = [model.τ]
desired_poles = [[-5, -6], [-5, -6]]  # 2 poles for relative degree 2, two barrier functions
filter_mtk, x_mtk, p_mtk, sys = exponential_safetyfilter(model, inputs, [model.h1, model.h2]; umin=-5, umax=5, desired_poles, split=false)

θiωi = [findfirst(isequal(var), x_mtk) for var in (model.θ, model.ω)] # Indexing helper

# Nominal controller: simple PD control
function u_nom(x, p, t)
    θ, ω  = x[θiωi]
    SA[10*(0 - θ) - 2*ω]  # PD control to origin
end

# Simulate with safety filter
x0 = ModelingToolkit.get_u0(sys, inputs .=> 0)
p0 = ModelingToolkit.get_p(sys, inputs .=> 0)
sol = DyadControlSystems.simulate(filter_mtk, x0, u_nom, (0.0, 5.0); p=p0)
sol_unsafe = DyadControlSystems.simulate(filter_mtk, x0, u_nom, (0.0, 5.0); Ts=0.01, p=p0, safety=false)

# Plot results
plot(sol, lw=2, layout=5, margin=2Plots.mm, titlefontsize=10, size=(800,600))
plot!(sol_unsafe.t, [sol_unsafe.h; sol_unsafe.x; sol_unsafe.u]', l=(:dash, :orange), label="Without safety filter", sp=(1:5)')
title!("Angle (rad)", sp=θiωi[1]+2)
title!("Angular velocity (rad/s)", sp=θiωi[2]+2)
hline!([deg2rad(5)], label="Safety limit", ls=:dash, color=:red, sp=θiωi[1]+2)
Example block output

Note, the second barrier function, $h_2$, is negative at the initial condition. However, once it enters the safe set, it remains there.

Lyapunov-Based Optimal Control with Safety Constraints

The lyapunov_opt function provides a unified framework for control design that combines:

  • Quadratic cost minimization: Tracks a reference control while minimizing control effort
  • Lyapunov stability guarantees: Ensures the closed-loop system is stable
  • Optional safety constraints: Can incorporate exponential control barrier functions

The controller solves the following optimization problem at each time step:

\[\begin{aligned} \min_{u, \delta} \quad & \|u - u_{\text{ref}}\|^2_H + p_2 \delta^2 + p_1\delta \\ \text{subject to} \quad & L_f V + L_g V \cdot u \leq -\gamma(V(x)) + \delta \\ & L_g L_f^{\nu-1} h \cdot u \geq -L_f^\nu h - K_\alpha z \quad \text{(if safety enabled)} \\ & u_{\min} \leq u \leq u_{\max} \\ & \delta \geq 0 \end{aligned}\]

where:

  • $V(x)$ is a Lyapunov function (e.g., system energy)
  • $\gamma$ is a class-$\mathcal{K}$ function controlling the convergence rate
  • $\delta$ is a slack variable allowing soft Lyapunov constraints
  • $p_2$ is the quadratic penalty weight for the slack variable
  • $p_1$ is the linear penalty weight for the slack variable
  • $h(x)$ are optional barrier functions for safety
  • $H$ is the weight matrix for control cost
  • $u_{\text{ref}}$ is an optional nominal control input. This may be used to cause the controller to act like a safety filter, or left out to yield a pure optimization-based Lyapunov controller

Example 4: Pendulum with Lyapunov Control and Safety

This example demonstrates how to combine Lyapunov-based stabilization with safety constraints for a pendulum system. This example builds upon the previous pendulum example, adding a Lyapunov function based on total mechanical energy and comparing performance with and without safety constraints.

using ModelingToolkit
using ModelingToolkit: t_nounits as t, D_nounits as D
using LinearAlgebra

# Define pendulum with Lyapunov function
@mtkmodel PendulumWithLyapunov begin
    @parameters begin
        m = 1.0
        l = 1.0
        g = 9.81
        b = 0.1  # Damping
    end
    @variables begin
        θ(t) = π/6     # Initial angle
        ω(t) = 0.0      # Initial angular velocity
        τ(t)            # Control torque
        V(t)            # Lyapunov function (total energy)
        h1(t)           # Barrier function 1
        h2(t)           # Barrier function 2
    end
    @equations begin
        D(θ) ~ ω
        D(ω) ~ (-g/l*sin(θ) - b*ω + τ)/(m*l^2)
        # V ~ θ^2 + ω^2
        V ~ (1/2)*m*l^2*ω^2 + m*g*l*(1 - cos(θ))  # Total mechanical energy
        h1 ~ deg2rad(20) - θ  # Keep angle < 20 degrees
        h2 ~ deg2rad(8) + θ  # Keep angle > -8 degrees
    end
end

@named pend = PendulumWithLyapunov()
model = complete(pend)

# Create Lyapunov controller WITHOUT safety constraints
H = [1.0;;]  # Weight for control cost
gamma = x -> 5x  # Fast convergence rate
umin = -3
umax = 3
p2_delta = 1  # High penalty on slack variable to enforce Lyapunov constraint
p1_delta = 1
inputs = [model.τ]
soft_safety = true
controller_nosafety, x_ns, p_ns, sys_ns = DyadControlSystems.lyapunov_opt(
    model, inputs, model.V;
    p2_delta, p1_delta, gamma, soft_safety,
    H, umin, umax, split=false)

# Create Lyapunov controller WITH safety constraints
controller_safety, x_s, p_s, sys_s = DyadControlSystems.lyapunov_opt(
    model, inputs, model.V;
    p2_delta, p1_delta, gamma, soft_safety,
    H, umin, umax,
    h=[model.h1, model.h2],  # Add safety constraints
    desired_poles=[[-5, -6], [-5, -6]], split=false)  # Fast safety response

# Get initial conditions and parameters
x0 = ModelingToolkit.get_u0(sys_s, inputs .=> 0)
p0 = ModelingToolkit.get_p(sys_s, inputs .=> 0)

# Reference control: energy-optimal (zero reference minimizes energy)
function u_nom(x, p, t)
    θ, ω  = x[θiωi]
    0*SA[10*(0 - θ) - 2*ω]  # PD control to origin, multiplied by 0 to yield zero nominal control
end

# Simulate both controllers
sol_nosafety = DyadControlSystems.simulate(controller_nosafety, x0, u_nom, (0, 8); p=p0)
sol_safety   = DyadControlSystems.simulate(controller_safety,   x0, u_nom, (0, 8); p=p0)

# Find indices for θ and ω
θi = findfirst(isequal(model.θ), x_s)
ωi = findfirst(isequal(model.ω), x_s)

# Plot comparison
p1 = plot(sol_nosafety.t, rad2deg.(sol_nosafety.x[θi, :]),
    label="Without safety", lw=2, color=:blue,
    xlabel="Time (s)", ylabel="Angle (degrees)",
    title="Pendulum Angle: Lyapunov Control")
plot!(sol_safety.t, rad2deg.(sol_safety.x[θi, :]),
    label="With safety", lw=2, color=:green)
hline!([20, -8], label="Safety limits", ls=:dash, color=:red, lw=1)

p2 = plot(sol_nosafety.t, sol_nosafety.V,
    label="Without safety", lw=2, color=:blue,
    xlabel="Time (s)", ylabel="Lyapunov function V",
    title="Energy Evolution")
plot!(sol_safety.t, sol_safety.V,
    label="With safety", lw=2, color=:green)

p3 = plot(sol_nosafety.t, sol_nosafety.u[1, :],
    label="Without safety", lw=2, color=:blue,
    xlabel="Time (s)", ylabel="Control torque τ",
    title="Control Input")
plot!(sol_safety.t, sol_safety.u[1, :],
    label="With safety", lw=2, color=:green)
hline!([3, -3], label="Control limits", ls=:dot, color=:gray, lw=1)

# Plot barrier functions for safety controller
p4 = plot(sol_safety.t, sol_safety.h[1, :],
    label="h₁ (upper bound)", lw=2, color=:orange,
    xlabel="Time (s)", ylabel="Barrier function",
    title="Safety Constraints")
plot!(sol_safety.t, sol_safety.h[2, :],
    label="h₂ (lower bound)", lw=2, color=:purple)
hline!([0], label="Safety boundary", ls=:dash, color=:red, lw=1)
p5 = plot(sol_nosafety.t, sol_nosafety.delta,
    label="Slack variable δ", lw=2, color=:blue,
    xlabel="Time (s)", ylabel="Slack variable",
    title="Lyapunov Slack Variable")
plot!(p5, sol_safety.t, sol_safety.delta,
    label="Without safety", lw=2,color=:green)
p6 = plot(sol_nosafety.t, sol_nosafety.exitflags,
    label="Exit flag", lw=2, color=:blue,
    xlabel="Time (s)", ylabel="Exit flag",
    title="QP Exit Flag")
plot!(p6, sol_safety.t, sol_safety.exitflags,
    label="With safety", lw=2,color=:green)
plot(p1, p2, p3, p4, p5, p6, size=(1000, 700), xlabel="")
Example block output

The plots show:

  • Angle trajectory: The safety-constrained controller respects the limits while the unconstrained controller violates them
  • Lyapunov function: Both controllers decrease the system energy
  • Control input: Safety constraints modify the control when approaching boundaries
  • QP exit flag: Indicates whether an optimal solution was found (1) or if constraint softening was needed (2)

Practical Considerations

When to Use Safety Filters

Safety filters are most effective when:

  • Hard constraints must be satisfied (collision avoidance, actuator limits)
  • A nominal controller exists but lacks safety guarantees
  • Real-time computation is feasible (QP can be solved quickly)
  • System model is reasonably accurate near constraints

Limitations

  • Requires accurate model near safety boundaries
  • QP must be feasible (conflicting constraints due to actuator limitations can cause issues). The safety filter cannot predict future infeasibility due to actuator limitations.
  • Does not handle stochastic uncertainties directly

Integration with Other Controllers

Safety filters work well with:

  • Feedback linearization: Apply safety filter to the computed $u$
  • MPC: Use as a backup safety layer, e.g., when using linear MPC but needing nonlinear safety guarantees

Index

DyadControlSystems.safetyfilterFunction
safetyfilter(f, g, h, x; umax, umin, a=identity, p=[])

Create a SafetyFilter that computes safe control inputs u_opt by solving a quadratic program (QP) that minimizes the deviation from a nominal control input u_nom while ensuring safety constraints defined by a control barrier function h(x).

This function requires $h$ to have relative degree 1 with respect to the system dynamics defined by f and g, that is, the time derivative of $h$ must depend explicitly on the control input u. For barrier functions with higher relative degree, consider using exponential_safetyfilter.

Arguments:

  • f: Symbolic drift dynamics f(x)
  • g: Symbolic input dynamics g(x)
  • h: Symbolic barrier function h(x)
  • x: Symbolic state variables
  • umax: Upper bounds on control inputs
  • umin: Lower bounds on control inputs
  • a: Barrier function scaling (default: identity). Must be a class K function, i.e., strictly increasing and a(0) = 0.
  • p: Symbolic parameters (default: [])
  • soft_safety: Whether to use soft constraints for safety (default: true) Use these arguments to prioritize safety vs. actuator constraints.
  • soft_input: Whether to use soft constraints for input bounds (default: false)

Returns:

  • SafetyFilter: A struct that can be called as filter(x, u_nom, p, t) to compute safe control

Example:

filter = safetyfilter(f, g, h, x; umax=10, umin=-10)
u_safe, fval, exitflag, info = filter(x_current, u_nominal, [], 0.0)
source
filter, x, p, sys = safetyfilter(model::System, inputs, h; umax, umin, a=identity, split=true)

Create a safety filter from a ModelingToolkit model.

Arguments:

  • model: ModelingToolkit System
  • inputs: Vector of input variables
  • h: Barrier function (scalar Num or expression in terms of model variables)
  • umax: Upper bounds on control inputs
  • umin: Lower bounds on control inputs
  • a: Barrier function scaling (default: identity)
  • split: Whether to split the system (default: true)

Returns:

  • filter: SafetyFilter object that can be called as u_safe = filter(x, u_nom, p, t)
  • x: State variables (symbolic). Use ModelingToolkit.get_u0 to obtain numeric initial condition.
  • p: Parameters (symbolic). Use ModelingToolkit.get_p to obtain numeric parameters.
  • sys: Simplified ModelingToolkit system

Example

@mtkmodel Pendulum begin
    @parameters begin
        m = 1.0; l = 1.0; g = 9.81
    end
    @variables begin
        θ(t) = π/6
        ω(t) = 0.0
        τ(t)  # Control input
    end
    @equations begin
        D(θ) ~ ω
        D(ω) ~ (-g/l*sin(θ) + τ)/(m*l^2)
    end
end

@named pend = Pendulum()
model = complete(pend)

# Safety constraint: keep angle smaller than 45 degrees
h_safety = π/4 - model.θ

# Create safety filter
filter, x, p, sys = safetyfilter(model, [model.τ], h_safety; umin=-5, umax=5)
source
DyadControlSystems.SafetyFilterType
SafetyFilter

A structure containing compiled functions and QP data for computing safe control inputs.

Fields:

  • f_sym: Symbolic drift dynamics f(x)
  • g_sym: Symbolic input dynamics g(x)
  • h_sym: Symbolic barrier function h(x)
  • Lfh_sym: Symbolic Lie derivative L_f h
  • Lgh_sym: Symbolic Lie derivative L_g h
  • h_fun: Compiled function for h(x)
  • Lfh_fun: Compiled function for L_f h(x)
  • Lgh_fun: Compiled function for L_g h(x)
  • H: QP objective matrix
  • f_qp: QP objective vector (mutable)
  • A: QP constraint matrix (mutable)
  • b_lower: Lower bounds for constraints (mutable)
  • b_upper: Upper bounds for constraints (mutable)
  • sense: Constraint sense vector
  • a: Barrier function scaling (e.g., identity or custom function)
  • nu: Number of inputs
  • ny: Number of outputs/constraints

The struct can be called as a functor: filter(x, u_nom) returns the safe control input.

source
DyadControlSystems.exponential_safetyfilterFunction
exponential_safetyfilter(f, g, h, x; umax, umin, K_alpha=nothing, desired_poles=nothing, p=[], soft_safety = true, soft_input = false)

Create an ExponentialSafetyFilter for systems with relative degree > 1.

Arguments:

  • f: Symbolic drift dynamics f(x)
  • g: Symbolic input dynamics g(x)
  • h: Symbolic barrier function h(x)
  • x: Symbolic state variables
  • umax: Upper bounds on control inputs
  • umin: Lower bounds on control inputs
  • K_alpha: Gain vector for exponential stability (if nothing, computed from desired_poles)
  • desired_poles: Desired pole locations for computing K_alpha via pole placement
  • p: Symbolic parameters (default: [])
  • soft_safety: Whether to use soft constraints for safety (default: true)
  • soft_input: Whether to use soft constraints for input bounds (default: false)

Returns:

  • ExponentialSafetyFilter: A struct that can be called as filter(x, u_nom, p, t)

Example:

# System with relative degree 2
@variables x[1:3]
x = collect(x)
f = [x[2]; sin(x[3]); 0]
g = [0; 0; 1]
h = x[1] - 1.0  # Safety constraint: x[1] ≥ 1.0

# Create filter with desired poles for exponential convergence
filter = exponential_safetyfilter(f, g, h, x; 
    umax=10, umin=-10, 
    desired_poles=[-1.0, -2.0])

# Use in control loop
u_safe, fval, exitflag, info = filter(x_current, u_nominal, [], t)
source
filter, x, p, sys = exponential_safetyfilter(model::System, inputs, h; umax, umin, K_alpha=nothing, desired_poles=nothing, split=true, soft_input=false, soft_safety=true)

Create an exponential safety filter from a ModelingToolkit model for systems with relative degree > 1.

Arguments:

  • model: ModelingToolkit System
  • inputs: Vector of input variables
  • h: Barrier function (scalar Num or expression in terms of model variables)
  • umax: Upper bounds on control inputs
  • umin: Lower bounds on control inputs
  • K_alpha: Gain vector for exponential convergence (optional, computed via pole placement if not provided)
  • desired_poles: Desired poles for exponential convergence (optional, defaults to -1, -2, -3, ...)
  • split: Whether to split the system (default: true)

Returns:

  • filter: ExponentialSafetyFilter object that can be called as u_safe = filter(x, u_nom, p, t)
  • x: State variables
  • p: Parameters
  • sys: Compiled ModelingToolkit system

Example

@mtkmodel TripleIntegrator begin
    @variables begin
        x1(t) = 0.5
        x2(t) = 0.0
        x3(t) = 0.0
        u(t)
    end
    @equations begin
        D(x1) ~ x2
        D(x2) ~ x3
        D(x3) ~ u
    end
end

@named sys = TripleIntegrator()
model = complete(sys)

# Safety constraint on position
h_safety = 1.0 - model.x1

# Create exponential safety filter with custom poles
filter, x, p, sys = exponential_safetyfilter(model, [model.u], h_safety; 
    umin=-10, umax=10, desired_poles=[-1, -2, -3])
source
DyadControlSystems.ExponentialSafetyFilterType
ExponentialSafetyFilter

A structure for computing safe control inputs using exponential control barrier functions (ECBF) for systems with relative degree greater than 1.

Fields:

  • f_sym: Symbolic drift dynamics f(x)
  • g_sym: Symbolic input dynamics g(x)
  • h_sym: Symbolic barrier function h(x)
  • f: Compiled function f(x,p,t)
  • g: Compiled function g(x,p,t)
  • h: Compiled function h(x,p,t)
  • T: Array of Lie derivatives h, Lf h, ..., Lf^(ν-1) h
  • T_fun: Compiled function for T(x,p,t)
  • Lfnu_h: Symbolic L_f^ν h (highest order Lie derivative)
  • LgLfnu_h: Symbolic Lg Lf^(ν-1) h
  • Lfnu_h_fun: Compiled function for L_f^ν h
  • LgLfnu_h_fun: Compiled function for Lg Lf^(ν-1) h
  • relative_degree: The relative degree ν
  • K_alpha: Gain vector for exponential stability (row vector)
  • H: QP objective matrix
  • f_qp: QP objective vector (mutable)
  • A: QP constraint matrix (mutable)
  • b_lower: Lower bounds for constraints (mutable)
  • b_upper: Upper bounds for constraints (mutable)
  • sense: Constraint sense vector
  • nu: Number of inputs
  • ny: Number of outputs/constraints
  • x_sym: Symbolic state variables
  • p_sym: Symbolic parameters
source
DyadControlSystems.lyapunov_optFunction
lyapunov_opt(f, g, V, x; H, umin, umax, gamma=identity, p2_delta=1e3, p1_delta=0, p=[], soft_input=false, soft_lyapunov=true, h=nothing, K_alpha=nothing, desired_poles=nothing, soft_safety=true)

Create a Lyapunov-based optimal controller that computes control inputs by solving a QP that minimizes a quadratic cost while ensuring Lyapunov stability and optionally safety constraints.

The optimization problem solved at each time step is:

minimize   (u - u_ref)'*H*(u - u_ref) + p2_delta * delta^2 + p1_delta * delta
subject to L_f V + L_g V * u ≤ -gamma(V(x)) + delta
           L_g L_f^(ν-1) h * u ≥ -L_f^ν h - K_α * η  (if h provided)
           umin ≤ u ≤ umax
           delta ≥ 0

Arguments:

  • f: Symbolic drift dynamics f(x)
  • g: Symbolic input dynamics g(x)
  • V: Symbolic Lyapunov function V(x)
  • x: Symbolic state variables
  • H: Weight matrix for control cost (positive definite)
  • umin: Lower bounds on control inputs
  • umax: Upper bounds on control inputs
  • gamma: Class K function for Lyapunov decay rate (default: identity). This function can be used to tune the agressiveness of the controller.
  • p2_delta: Quadratic penalty weight for slack variable delta (default: 1e3)
  • p1_delta: Linear penalty weight for slack variable delta (default: 0)
  • p: Symbolic parameters (default: [])
  • soft_input: Whether to use soft constraints for input bounds (default: false)
  • soft_lyapunov: Whether to use soft constraints for Lyapunov stability (default: true)
  • h: Optional symbolic barrier function(s) h(x) for safety constraints (default: nothing)
  • K_alpha: Gain matrix for exponential control barrier functions (if nothing, computed from desired_poles)
  • desired_poles: Desired pole locations for computing K_alpha via pole placement
  • soft_safety: Whether to use soft constraints for safety (default: true)

Returns:

  • LyapunovOptimalController: A struct that can be called as u_opt, delta_opt = controller(x, u_ref, p, t)

Example:

@variables x[1:2]
x = collect(x)
f = [-x[1]; -x[2]]
g = [1 0; 0 1]
V = x[1]^2 + x[2]^2  # Quadratic Lyapunov function
h = x[1] - 0.5  # Safety constraint: x[1] ≥ 0.5

H = I(2)  # Identity weight matrix
controller = lyapunov_opt(f, g, V, x; H=H, umin=[-1, -1], umax=[1, 1], h=h, desired_poles=[-1, -2])

# Use in control loop
x_current = [0.6, 0.3]
u_ref = [0.0, 0.0]  # Energy-optimal control
u_opt, delta_opt, fval, exitflag, info = controller(x_current, u_ref, [], 0.0)
source
controller, x, p, sys = lyapunov_opt(model::System, inputs, V; H, umin, umax, gamma=identity, p2_delta=1e3, p1_delta=0, h=nothing, K_alpha=nothing, desired_poles=nothing, soft_input=false, soft_lyapunov=true, soft_safety=true, split=true)

Create a Lyapunov-based optimal controller from a ModelingToolkit model.

Arguments:

  • model: ModelingToolkit System
  • inputs: Vector of input variables
  • V: Lyapunov function (scalar Num or expression in terms of model variables)
  • H: Weight matrix for control cost (positive definite)
  • umin: Lower bounds on control inputs
  • umax: Upper bounds on control inputs
  • gamma: Class K function for Lyapunov decay rate (default: identity)
  • p2_delta: Quadratic penalty weight for slack variable (default: 1e3)
  • p1_delta: Linear penalty weight for slack variable (default: 0)
  • h: Optional barrier function(s) for safety constraints (default: nothing)
  • K_alpha: Gain matrix for exponential control barrier functions (if nothing, computed from desired_poles)
  • desired_poles: Desired pole locations for computing K_alpha via pole placement
  • soft_input: Whether to use soft constraints for input bounds (default: false)
  • soft_lyapunov: Whether to use soft constraints for Lyapunov stability (default: true)
  • soft_safety: Whether to use soft constraints for safety (default: true)
  • split: Whether to split the system (default: true)

Returns:

  • controller: LyapunovOptimalController object that can be called as u_opt, delta_opt = controller(x, u_ref, p, t)
  • x: State variables (symbolic). Use ModelingToolkit.get_u0 to obtain numeric initial condition.
  • p: Parameters (symbolic). Use ModelingToolkit.get_p to obtain numeric parameters.
  • sys: Simplified ModelingToolkit system

Example

using ModelingToolkit, ModelingToolkit: t_nounits as t, D_nounits as D

@mtkmodel PendulumWithLyapunov begin
    @parameters begin
        m = 1.0; l = 1.0; g = 9.81; b = 0.1
    end
    @variables begin
        θ(t) = π/6
        ω(t) = 0.0
        τ(t)  # Control input
        V(t)  # Lyapunov function (total energy)
        h(t)  # Optional barrier function
    end
    @equations begin
        D(θ) ~ ω
        D(ω) ~ (-g/l*sin(θ) - b*ω + τ)/(m*l^2)
        V ~ (1/2)*m*l^2*ω^2 + m*g*l*(1 - cos(θ))  # Total energy
        h ~ π/4 - abs(θ)  # Keep angle within ±45 degrees
    end
end

@named pend = PendulumWithLyapunov()
model = complete(pend)

# Create Lyapunov controller with optional safety
H = 1.0  # Scalar for single input
controller, x, p, sys = lyapunov_opt(model, [model.τ], model.V;
    H=H, umin=-5, umax=5, h=model.h, desired_poles=[-2])

# Simulate
x0 = ModelingToolkit.get_u0(sys, [model.τ] .=> 0)
p0 = ModelingToolkit.get_p(sys, [model.τ] .=> 0)
u_ref_func = (x, p, t) -> [0.0]  # Energy-optimal control
sol = simulate(controller, x0, u_ref_func, (0.0, 10.0); p=p0)
source
DyadControlSystems.LyapunovOptimalControllerType
LyapunovOptimalController

A structure for computing optimal control inputs that minimize a quadratic cost while ensuring Lyapunov stability with a slack variable for soft constraints.

Fields:

  • f_sym: Symbolic drift dynamics f(x)
  • g_sym: Symbolic input dynamics g(x)
  • V_sym: Symbolic Lyapunov function V(x)
  • f: Compiled function f(x,p,t)
  • g: Compiled function g(x,p,t)
  • V_fun: Compiled function for V(x,p,t)
  • LfV_fun: Compiled function for L_f V(x,p,t)
  • LgV_fun: Compiled function for L_g V(x,p,t)
  • H_qp: QP objective matrix (includes both u and delta)
  • f_qp: QP objective vector (mutable)
  • A: QP constraint matrix (mutable)
  • b_lower: Lower bounds for constraints (mutable)
  • b_upper: Upper bounds for constraints (mutable)
  • sense: Constraint sense vector
  • gamma: Class K function for Lyapunov decay rate
  • nu: Number of control inputs
  • x_sym: Symbolic state variables
  • p_sym: Symbolic parameters
source
DyadControlSystems.LyapunovOptimalControllerSolutionType
LyapunovOptimalControllerSolution

Solution structure containing the results of a Lyapunov optimal control simulation.

Fields:

  • controller: The LyapunovOptimalController used for simulation
  • t: Time vector
  • x: State trajectories (nx × nT matrix)
  • V: Lyapunov function values (nT vector)
  • h: Barrier function values (ny × nT matrix) if ECBF constraints are present, nothing otherwise
  • u_ref: Reference control trajectories (nu × nT matrix)
  • u: Optimal control trajectories (nu × nT matrix)
  • delta: Slack variable values (nT vector)
  • exitflags: QP solver exit flags (nT vector)

Plot

The solution object can be plotted:

plot(sol; plotx=true, plotV=true, ploth=true, plotu=true, plotdelta=false)

where:

  • plotx: Plot the state trajectories
  • plotV: Plot the Lyapunov function values
  • ploth: Plot the barrier function values (if present)
  • plotu: Plot reference vs optimal control inputs
  • plotdelta: Show the slack variable values
source
DyadControlSystems.get_dynamicsFunction
dynamics(filter::ExponentialSafetyFilter)

Return a closure function (x, u, p, t) -> ẋ that computes the system dynamics ẋ = f(x) + g(x)*u.

This allows the filter's dynamics to be used directly with integrators.

Example

filter = exponential_safetyfilter(f, g, h, x; umax=10, umin=-10)
dyn = dynamics(filter)
integrator = SeeToDee.Rk4(dyn, dt)
x_next = integrator(x_current, u_current, [], t)
source
get_dynamics(filter::SafetyFilter)

Return a closure function (x, u, p, t) -> ẋ that computes the system dynamics ẋ = f(x) + g(x)*u.

This allows the filter's dynamics to be used directly with integrators.

Example

filter = safetyfilter(f, g, h, x; umax=10, umin=-10)
dyn = get_dynamics(filter)
integrator = SeeToDee.Rk4(dyn, dt)
x_next = integrator(x_current, u_current, [], t)
source
get_dynamics(ctrl::LyapunovOptimalController)

Return a closure function (x, u, p, t) -> ẋ that computes the system dynamics ẋ = f(x) + g(x)*u.

This allows the controller's dynamics to be used directly with integrators.

Example

ctrl = lyapunov_opt(f, g, V, x; H=I(2), umin=[-1,-1], umax=[1,1])
dyn = get_dynamics(ctrl)
integrator = SeeToDee.Rk4(dyn, dt)
x_next = integrator(x_current, u_current, [], t)
source
DyadControlSystems.simulateMethod
simulate(filter, x0, u_nom_func, tspan; Ts, p, kwargs...)

Simulate a system with a safety filter.

Arguments:

  • filter: SafetyFilter or ExponentialSafetyFilter
  • x0: Initial state vector
  • u_nom_func: Nominal controller function u_nom_func(x, p, t) -> u_nom
  • dynamics: System dynamics function dynamics(x, u, p, t) -> ẋ or nothing (uses filter's dynamics)
  • tspan: Time span (t0, tf) for simulation
  • Ts: Sample time (default: (tf-t0)/500)
  • p: Parameters (default: [])
  • p_plant: Plant parameters (default: p)
  • p_controller: Controller parameters (default: p)
  • p_filter: Filter parameters (default: p)
  • safety: Whether to apply the safety filter (default: true)

Returns:

Example:

# For ExponentialSafetyFilter (has built-in dynamics)
filter = exponential_safetyfilter(f, g, h, x; umax=10, umin=-10)
u_nom_func = (x, p, t) -> -K * x  # Simple linear controller
sol = simulate(filter, x0, u_nom_func, nothing, (0.0, 10.0))

# For SafetyFilter (needs external dynamics)
filter = safetyfilter(f, g, h, x; umax=1, umin=-1)
dynamics = (x, u, p, t) -> f(x) + g(x)*u  # Provide dynamics
sol = simulate(filter, x0, u_nom_func, dynamics, (0.0, 10.0))

# Plot the results
plot(sol)
source
DyadControlSystems.simulateMethod
simulate(ctrl::LyapunovOptimalController, x0, u_ref_func, tspan; Ts, p, kwargs...)

Simulate a system with a Lyapunov optimal controller.

Arguments:

  • ctrl: LyapunovOptimalController
  • x0: Initial state vector
  • u_ref_func: Reference controller function u_ref_func(x, p, t) -> u_ref
  • tspan: Time span (t0, tf) for simulation
  • Ts: Sample time (default: (tf-t0)/500)
  • p: Parameters (default: [])
  • p_plant: Plant parameters (default: p)
  • p_controller: Controller parameters for ureffunc (default: p)
  • p_ctrl: Controller parameters for optimization (default: p)

Returns:

Example:

ctrl = lyapunov_opt(f, g, V, x; H=I(2), umin=[-1,-1], umax=[1,1])
u_ref_func = (x, p, t) -> [0.0, 0.0]  # Zero reference for energy-optimal control
sol = simulate(ctrl, x0, u_ref_func, (0.0, 10.0))
plot(sol)
source