Chapter 3: Mathematical Preliminaries for Adaptive Laws

Lesson 5: Barbalat’s Lemma and Convergence for Adaptive Systems

This lesson develops the convergence argument most frequently used after a Lyapunov-based adaptive law has established boundedness. We prove Barbalat’s lemma, derive the forms used in adaptive control, and apply the result to a scalar state–parameter error model. The central distinction is that a nonincreasing Lyapunov function often proves boundedness and square integrability first; asymptotic tracking convergence requires an additional regularity argument, while exact parameter convergence requires still stronger information about the regressor.

1. Why Lyapunov Stability Is Not Yet Convergence

In the previous lessons, a combined state–parameter Lyapunov function was used to establish boundedness of both the tracking variables and the parameter estimates. A typical adaptive proof reaches an inequality of the form

\[ V(t) \ge 0, \qquad \dot V(t) = -W(e(t)), \qquad W(e) \ge 0. \]

From \( \dot V \le 0 \), one immediately obtains that \( V(t) \) is nonincreasing and bounded below. Therefore, it has a finite limit. Integration gives

\[ V(t)-V(0)=-\int_{0}^{t}W(e(\tau))\,d\tau, \]

and hence

\[ \int_{0}^{\infty}W(e(\tau))\,d\tau =V(0)-\lim_{t\longrightarrow\infty}V(t) < \infty. \]

This proves that the dissipation signal \( W(e(t)) \) is integrable. It does not, by itself, prove that \( W(e(t)) \longrightarrow 0 \). A continuous signal may contain increasingly narrow spikes whose total area is finite while its peak value never decreases. Barbalat’s lemma excludes this behavior by requiring uniform continuity.

flowchart TD
  A["Choose a nonnegative Lyapunov function V"] --> B["Derive Vdot = -W with W nonnegative"]
  B --> C["V is bounded and nonincreasing"]
  C --> D["The integral of W is finite"]
  C --> E["State and parameter signals are bounded"]
  E --> F["Use system dynamics to bound the derivative of W or the error"]
  F --> G["Obtain uniform continuity"]
  D --> H["Apply Barbalat convergence argument"]
  G --> H
  H --> I["Conclude the selected error signal tends to zero"]
        

2. Function Spaces and Uniform Continuity

Let \( f:[0,\infty)\longrightarrow\mathbb{R}^{m} \) be measurable. For \( 1\le p<\infty \), the \( L_p \) norm is

\[ \|f\|_{L_p} =\left(\int_{0}^{\infty}\|f(t)\|^{p}\,dt\right)^{1/p}. \]

The space \( L_\infty \) contains essentially bounded signals:

\[ \|f\|_{L_\infty} =\operatorname*{ess\,sup}_{t\ge 0}\|f(t)\| < \infty. \]

The statement \( f\in L_2 \) means that the signal has finite energy. It does not imply that the signal is absolutely integrable or that it possesses a pointwise limit.

A function \( f \) is uniformly continuous on \( [0,\infty) \) when

\[ \forall\varepsilon>0\;\exists\delta>0\;\forall t_1,t_2\ge0: |t_1-t_2|<\delta \;\Longrightarrow\; \|f(t_1)-f(t_2)\|<\varepsilon. \]

The same \( \delta \) must work everywhere on the unbounded time interval. A sufficient condition used repeatedly in adaptive control is bounded differentiability:

\[ \|\dot f(t)\|\le M \quad \forall t\ge0 \;\Longrightarrow\; \|f(t_2)-f(t_1)\|\le M|t_2-t_1|. \]

Thus a bounded derivative makes \( f \) globally Lipschitz and therefore uniformly continuous. Merely proving that \( f \) is bounded is insufficient; for example, \( \sin(t^2) \) is bounded but not uniformly continuous on \( [0,\infty) \).

3. Barbalat’s Lemma

Theorem (classical integral form). Let

\[ f:[0,\infty)\longrightarrow\mathbb{R} \]

be uniformly continuous. Suppose the improper integral has a finite limit:

\[ \lim_{t\longrightarrow\infty}\int_{0}^{t}f(\tau)\,d\tau \quad\text{exists and is finite}. \]

Then

\[ \lim_{t\longrightarrow\infty}f(t)=0. \]

For Lyapunov analysis, the most common special case is \( f(t)\ge0 \) and \( f\in L_1 \). The theorem then states that a nonnegative, uniformly continuous, integrable signal must vanish asymptotically.

4. Proof by Contradiction

Assume the conclusion is false. Then there exists \( \varepsilon_0>0 \) and a sequence \( t_k\longrightarrow\infty \) such that

\[ |f(t_k)|\ge\varepsilon_0. \]

Uniform continuity with tolerance \( \varepsilon_0/2 \) supplies a number \( \delta>0 \), independent of \( k \), such that

\[ |t-t_k|<\delta \;\Longrightarrow\; |f(t)-f(t_k)|<\frac{\varepsilon_0}{2}. \]

Therefore, on the interval \( [t_k,t_k+\delta] \), the function keeps the sign of \( f(t_k) \) and satisfies

\[ |f(t)|\ge\frac{\varepsilon_0}{2}. \]

Consequently,

\[ \left|\int_{t_k}^{t_k+\delta}f(\tau)\,d\tau\right| \ge\frac{\varepsilon_0\delta}{2}. \]

On the other hand, convergence of the improper integral implies its Cauchy property: for every \( \eta>0 \), there is a time \( T \) such that

\[ t_2\ge t_1\ge T \;\Longrightarrow\; \left|\int_{t_1}^{t_2}f(\tau)\,d\tau\right|<\eta. \]

Choosing \( \eta=\varepsilon_0\delta/4 \) and a sufficiently large \( k \) contradicts the preceding lower bound. Hence \( f(t)\longrightarrow0 \).

5. Corollaries Used in Adaptive Control

5.1 Uniform continuity plus finite \(L_p\) norm

Let \( f:[0,\infty)\longrightarrow\mathbb{R}^{m} \) be uniformly continuous. If \( f\in L_p \) for any finite \( p>0 \), then \( f(t)\longrightarrow0 \). Otherwise, uniform continuity would generate infinitely many intervals of fixed width on which \( \|f\| \) remains bounded away from zero, forcing the \( L_p \) integral to diverge.

5.2 The standard \(L_2\)–bounded-derivative test

If

\[ f\in L_2, \qquad \dot f\in L_\infty, \]

then

\[ \lim_{t\longrightarrow\infty}f(t)=0. \]

Boundedness of \( \dot f \) makes \( f \) uniformly continuous; square integrability then invokes the previous corollary. This is the version used most often for a scalar or vector tracking error.

5.3 Dissipation-function form

Suppose

\[ \dot V(t)=-W(t), \qquad V(t)\ge V_{\min}, \qquad W(t)\ge0. \]

Then

\[ \int_{0}^{\infty}W(t)\,dt \le V(0)-V_{\min} < \infty. \]

If \( W \) is uniformly continuous—for example if \( \dot W\in L_\infty \)—then \( W(t)\longrightarrow0 \). When \( W(e)=e^{\mathsf T}Qe \) with \( Q=Q^{\mathsf T}>0 \), this implies \( e(t)\longrightarrow0 \) because

\[ \lambda_{\min}(Q)\|e(t)\|^2 \le e(t)^{\mathsf T}Qe(t)=W(e(t)). \]

6. A Reusable Adaptive-System Convergence Template

Consider a tracking error \( e\in\mathbb{R}^{n} \), an unknown constant parameter vector \( \theta^{\ast}\in\mathbb{R}^{q} \), an estimate \( \hat\theta \), and parameter error

\[ \tilde\theta=\theta^{\ast}-\hat\theta. \]

A common Lyapunov candidate combines state and parameter energies:

\[ V(e,\tilde\theta) =e^{\mathsf T}Pe +\tilde\theta^{\mathsf T}\Gamma^{-1}\tilde\theta, \]

where \( P=P^{\mathsf T}>0 \) and \( \Gamma=\Gamma^{\mathsf T}>0 \). The adaptive law is chosen to cancel the state–parameter cross term, producing

\[ \dot V=-e^{\mathsf T}Qe, \qquad Q=Q^{\mathsf T}>0. \]

The proof then follows five distinct steps:

  1. Boundedness: positive definiteness and \( V(t)\le V(0) \) imply \( e,\tilde\theta,\hat\theta\in L_\infty \).
  2. Finite error energy: \( e^{\mathsf T}Qe\in L_1 \), hence \( e\in L_2 \).
  3. Bound the error derivative: substitute the already bounded closed-loop signals into the error dynamics and prove \( \dot e\in L_\infty \).
  4. Apply Barbalat: \( e\in L_2 \) and \( \dot e\in L_\infty \) imply \( e(t)\longrightarrow0 \).
  5. Do not overclaim: boundedness of \( \tilde\theta \) does not imply \( \tilde\theta(t)\longrightarrow0 \). Exact parameter recovery requires additional information about the regressor, studied later in the course.

7. Scalar Adaptive Error Model

To isolate the convergence mechanism without introducing a complete MRAC architecture, consider the scalar error model

\[ \dot e=-a e+\phi(t)\tilde\theta, \qquad a>0, \]

with

\[ \tilde\theta=\theta^{\ast}-\hat\theta. \]

The regressor \( \phi(t) \) is assumed bounded and piecewise continuously differentiable. Choose the gradient-type update law

\[ \dot{\hat\theta}=\gamma\phi(t)e, \qquad \gamma>0. \]

Since \( \theta^{\ast} \) is constant,

\[ \dot{\tilde\theta}=-\gamma\phi(t)e. \]

Take

\[ V(e,\tilde\theta) =\frac{1}{2}e^2+\frac{1}{2\gamma}\tilde\theta^2. \]

Differentiation gives

\[ \begin{aligned} \dot V &=e\dot e+\frac{1}{\gamma}\tilde\theta\dot{\tilde\theta} \\ &=e(-ae+\phi\tilde\theta) +\frac{1}{\gamma}\tilde\theta(-\gamma\phi e) \\ &=-ae^2. \end{aligned} \]

The cancellation of \( e\phi\tilde\theta \) is the defining Lyapunov design step. It does not make the parameter error negative definite; instead, it removes the uncertain cross term from \( \dot V \).

8. Complete Convergence Proof for the Scalar Model

Step 1: Boundedness

Because

\[ \dot V=-ae^2\le0, \]

one has \( V(t)\le V(0) \). Therefore,

\[ |e(t)|\le\sqrt{2V(0)}, \qquad |\tilde\theta(t)|\le\sqrt{2\gamma V(0)}. \]

Thus \( e,\tilde\theta,\hat\theta\in L_\infty \).

Step 2: Square integrability of the tracking error

Integrating the Lyapunov derivative yields

\[ a\int_{0}^{t}e^2(\tau)\,d\tau=V(0)-V(t)\le V(0). \]

Taking the limit gives

\[ \int_{0}^{\infty}e^2(\tau)\,d\tau \le\frac{V(0)}{a} < \infty, \]

so \( e\in L_2 \).

Step 3: Uniform continuity

If \( \phi\in L_\infty \), then the error dynamics give

\[ |\dot e| \le a|e|+|\phi||\tilde\theta|. \]

Every term on the right is bounded, so \( \dot e\in L_\infty \). Hence \( e \) is uniformly continuous.

Step 4: Barbalat convergence

Since

\[ e\in L_2, \qquad \dot e\in L_\infty, \]

Barbalat’s corollary gives

\[ \boxed{\lim_{t\longrightarrow\infty}e(t)=0}. \]

Step 5: Behavior of the adaptive-law derivative

Bounded \( \phi \) and \( e(t)\longrightarrow0 \) imply

\[ \dot{\hat\theta}(t)=\gamma\phi(t)e(t) \longrightarrow0. \]

A derivative tending to zero does not prove that the parameter estimate approaches the true parameter. It only states that adaptation eventually slows. The estimate can settle at a constant different from \( \theta^{\ast} \).

9. Tracking Convergence Versus Parameter Convergence

The Lyapunov derivative depends on \( e \) but contains no negative-definite term in \( \tilde\theta \):

\[ \dot V=-ae^2. \]

Consequently, the proof forces the tracking error to zero but only keeps the parameter error bounded. At steady tracking, \( e=0 \), the error dynamics reduce to

\[ 0=\phi(t)\tilde\theta(t). \]

If the regressor becomes zero or loses sufficient variation, this equation can hold with a nonzero parameter error. For the computational example we choose

\[ \phi(t)=e^{-\lambda t}, \qquad \lambda>0. \]

The regressor fades away, so the tracking error can converge while the estimate freezes before reaching the true value. This is not a failure of the Barbalat argument: Barbalat proves exactly the convergence claim for which its hypotheses were established.

flowchart TD
  A["Lyapunov proof completed"] --> B["Tracking error has \nfinite energy"]
  B --> C["Tracking error derivative \nis bounded"]
  C --> D["Tracking error tends to zero"]
  A --> E["Parameter estimate is bounded"]
  E --> F["Does the regressor retain \nenough independent variation?"]
  F -->|"not established"| G["Parameter error may approach \na nonzero constant"]
  F -->|"additional richness condition"| H["Exact parameter convergence \nmay be provable later"]
        

10. Applying Barbalat Directly to the Dissipation Signal

In the scalar example, define \( W(t)=ae^2(t) \). Since \( \dot V=-W \) and \( V \) is bounded below, \( W\in L_1 \). Its derivative is

\[ \dot W(t)=2ae(t)\dot e(t). \]

Boundedness of \( e \) and \( \dot e \) implies \( \dot W\in L_\infty \). Therefore, \( W \) is uniformly continuous. Barbalat’s lemma yields

\[ W(t)=ae^2(t)\longrightarrow0, \]

which again implies \( e(t)\longrightarrow0 \).

The two proof routes are equivalent in this example:

\[ \begin{array}{c} e\in L_2,\;\dot e\in L_\infty \;\Longrightarrow\;e\longrightarrow0, \\ W\in L_1,\;\dot W\in L_\infty \;\Longrightarrow\;W\longrightarrow0 \;\Longrightarrow\;e\longrightarrow0. \end{array} \]

The first route is usually shorter when the error dynamics directly give a bound on \( \dot e \). The second is useful when the Lyapunov derivative naturally produces a more complicated dissipation function \( W \).

11. Common Proof Errors and Diagnostic Checks

  1. Claiming asymptotic convergence from \(\dot V\le0\) alone. This establishes monotonicity of \( V \), not necessarily convergence of each state.
  2. Using integrability without uniform continuity. A finite-area spike train is continuous and integrable but need not tend to zero.
  3. Assuming boundedness implies uniform continuity. Bounded oscillations may accelerate indefinitely. A bounded derivative is a convenient sufficient condition.
  4. Declaring \(\dot e\) bounded without checking every term. Regressors, reference signals, nonlinearities, and control inputs must each be bounded by earlier proof steps or explicit assumptions.
  5. Confusing tracking convergence with identification. The condition \( e(t)\longrightarrow0 \) does not generally imply \( \hat\theta(t)\longrightarrow\theta^{\ast} \).
  6. Ignoring solution existence. Barbalat’s lemma is an asymptotic argument; the closed-loop solution must first exist for all \( t\ge0 \). In standard smooth finite-dimensional models, boundedness of the solution prevents finite escape.

12. Numerical Experiment and Expected Results

The implementations use

\[ a=1, \qquad \gamma=2, \qquad \theta^{\ast}=2.5, \qquad \phi(t)=e^{-0.2t}, \]

with initial conditions

\[ e(0)=1.5, \qquad \hat\theta(0)=-1. \]

A fourth-order Runge–Kutta method or an adaptive ODE solver integrates the two coupled equations. The simulation checks the following numerical consequences of the proof:

  • \( V(t) \) is nonincreasing up to numerical error.
  • The accumulated integral \( \int_{0}^{t}e^2(\tau)d\tau \) approaches a finite value.
  • \( e(t) \) approaches zero.
  • \( \tilde\theta(t) \) approaches a generally nonzero constant because the regressor decays.

With the listed parameters and a small integration step, the expected final values near \( t=40 \) are approximately

\[ e(40)\approx-4.32\times10^{-5}, \qquad \tilde\theta(40)\approx-1.03\times10^{-1}. \]

These values illustrate the precise theorem: the tracking error converges, but exact parameter identification was not established.

13. Python Implementation

This implementation uses NumPy for array operations, Matplotlib for diagnostics, and an explicit RK4 integrator written from scratch. It exports the simulated signals to CSV.

Chapter3_Lesson5.py

"""Chapter 3, Lesson 5: Barbalat's Lemma and convergence.

Simulates the scalar adaptive error model
    e_dot         = -a*e + phi(t)*theta_tilde
    theta_hat_dot = gamma*phi(t)*e
where theta_tilde = theta_star - theta_hat and phi(t)=exp(-lambda*t).

The Lyapunov derivative is exactly V_dot = -a*e**2.  Because the
regressor decays, the tracking error converges while the parameter error
need not converge to zero.
"""

from __future__ import annotations

import csv
from dataclasses import dataclass
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


@dataclass(frozen=True)
class Parameters:
    a: float = 1.0
    gamma: float = 2.0
    theta_star: float = 2.5
    regressor_decay: float = 0.20


def regressor(t: float, p: Parameters) -> float:
    return float(np.exp(-p.regressor_decay * t))


def dynamics(t: float, state: np.ndarray, p: Parameters) -> np.ndarray:
    e, theta_hat = state
    theta_tilde = p.theta_star - theta_hat
    phi = regressor(t, p)
    e_dot = -p.a * e + phi * theta_tilde
    theta_hat_dot = p.gamma * phi * e
    return np.array([e_dot, theta_hat_dot], dtype=float)


def rk4_step(t: float, state: np.ndarray, dt: float, p: Parameters) -> np.ndarray:
    k1 = dynamics(t, state, p)
    k2 = dynamics(t + 0.5 * dt, state + 0.5 * dt * k1, p)
    k3 = dynamics(t + 0.5 * dt, state + 0.5 * dt * k2, p)
    k4 = dynamics(t + dt, state + dt * k3, p)
    return state + (dt / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)


def simulate(t_final: float = 40.0, dt: float = 0.002) -> dict[str, np.ndarray]:
    if t_final <= 0.0 or dt <= 0.0:
        raise ValueError("t_final and dt must be positive")

    p = Parameters()
    time = np.arange(0.0, t_final + dt, dt)
    state = np.empty((time.size, 2), dtype=float)
    state[0] = np.array([1.5, -1.0], dtype=float)

    for k in range(time.size - 1):
        state[k + 1] = rk4_step(time[k], state[k], dt, p)

    error = state[:, 0]
    theta_hat = state[:, 1]
    theta_tilde = p.theta_star - theta_hat
    phi = np.exp(-p.regressor_decay * time)
    error_dot = -p.a * error + phi * theta_tilde
    lyapunov = 0.5 * error**2 + 0.5 * theta_tilde**2 / p.gamma
    lyapunov_dot = -p.a * error**2
    integral_error_sq = np.concatenate(
        ([0.0], np.cumsum(0.5 * (error[:-1] ** 2 + error[1:] ** 2) * dt))
    )

    return {
        "time": time,
        "error": error,
        "theta_hat": theta_hat,
        "theta_tilde": theta_tilde,
        "phi": phi,
        "error_dot": error_dot,
        "lyapunov": lyapunov,
        "lyapunov_dot": lyapunov_dot,
        "integral_error_sq": integral_error_sq,
    }


def save_csv(data: dict[str, np.ndarray], path: Path) -> None:
    keys = list(data.keys())
    with path.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.writer(handle)
        writer.writerow(keys)
        writer.writerows(zip(*(data[key] for key in keys)))


def main() -> None:
    data = simulate()
    tolerance = 1e-10
    max_v_increase = float(np.max(np.diff(data["lyapunov"])))

    print(f"Final tracking error: {data['error'][-1]: .6e}")
    print(f"Final parameter error: {data['theta_tilde'][-1]: .6e}")
    print(f"Approximate integral of e(t)^2: {data['integral_error_sq'][-1]: .6f}")
    print(f"Maximum absolute e_dot: {np.max(np.abs(data['error_dot'])): .6f}")
    print(f"Maximum numerical increase in V: {max_v_increase: .3e}")
    print("V is numerically nonincreasing:", max_v_increase <= tolerance)

    output_dir = Path(__file__).resolve().parent
    save_csv(data, output_dir / "Chapter3_Lesson5_results.csv")

    plt.figure(figsize=(9, 5))
    plt.plot(data["time"], data["error"], label="tracking error e(t)")
    plt.plot(data["time"], data["theta_tilde"], label="parameter error theta_tilde(t)")
    plt.xlabel("time")
    plt.ylabel("signal value")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()
    plt.savefig(output_dir / "Chapter3_Lesson5_states.png", dpi=180)

    plt.figure(figsize=(9, 5))
    plt.plot(data["time"], data["lyapunov"], label="V(t)")
    plt.plot(data["time"], data["integral_error_sq"], label="integral_0^t e(tau)^2 d tau")
    plt.xlabel("time")
    plt.ylabel("diagnostic value")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()
    plt.savefig(output_dir / "Chapter3_Lesson5_diagnostics.png", dpi=180)
    plt.show()


if __name__ == "__main__":
    main()

14. C++ Implementation

The C++17 version uses only the standard library. It implements RK4, verifies numerical monotonicity of the Lyapunov function, and writes a CSV file suitable for plotting with another tool.

Chapter3_Lesson5.cpp

// Chapter 3, Lesson 5: Barbalat's Lemma and convergence.
// Standard-library RK4 simulation of an adaptive error system.

#include <algorithm>
#include <array>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <string>

struct Parameters {
    double a{1.0};
    double gamma{2.0};
    double theta_star{2.5};
    double regressor_decay{0.20};
};

using State = std::array<double, 2>;  // [e, theta_hat]

[[nodiscard]] double regressor(double t, const Parameters& p) {
    return std::exp(-p.regressor_decay * t);
}

[[nodiscard]] State dynamics(double t, const State& state, const Parameters& p) {
    const double e = state[0];
    const double theta_hat = state[1];
    const double theta_tilde = p.theta_star - theta_hat;
    const double phi = regressor(t, p);
    return State{-p.a * e + phi * theta_tilde, p.gamma * phi * e};
}

[[nodiscard]] State add_scaled(const State& x, const State& k, double scale) {
    return State{x[0] + scale * k[0], x[1] + scale * k[1]};
}

[[nodiscard]] State rk4_step(double t, const State& x, double dt, const Parameters& p) {
    const State k1 = dynamics(t, x, p);
    const State k2 = dynamics(t + 0.5 * dt, add_scaled(x, k1, 0.5 * dt), p);
    const State k3 = dynamics(t + 0.5 * dt, add_scaled(x, k2, 0.5 * dt), p);
    const State k4 = dynamics(t + dt, add_scaled(x, k3, dt), p);

    return State{
        x[0] + (dt / 6.0) * (k1[0] + 2.0 * k2[0] + 2.0 * k3[0] + k4[0]),
        x[1] + (dt / 6.0) * (k1[1] + 2.0 * k2[1] + 2.0 * k3[1] + k4[1])};
}

int main() {
    try {
        const Parameters p{};
        const double t_final = 40.0;
        const double dt = 0.002;
        if (t_final <= 0.0 || dt <= 0.0) {
            throw std::invalid_argument("t_final and dt must be positive");
        }

        State state{1.5, -1.0};
        double integral_e2 = 0.0;
        double previous_e2 = state[0] * state[0];
        double previous_v = 0.5 * previous_e2
                          + 0.5 * std::pow(p.theta_star - state[1], 2) / p.gamma;
        double max_v_increase = -1.0e300;
        double max_abs_e_dot = 0.0;

        std::ofstream csv("Chapter3_Lesson5_results_cpp.csv");
        if (!csv) {
            throw std::runtime_error("Cannot create output CSV file");
        }
        csv << "time,error,theta_hat,theta_tilde,phi,error_dot,lyapunov,lyapunov_dot,integral_error_sq\n";
        csv << std::setprecision(15);

        const std::size_t steps = static_cast<std::size_t>(std::llround(t_final / dt));
        for (std::size_t k = 0; k <= steps; ++k) {
            const double t = static_cast<double>(k) * dt;
            const double e = state[0];
            const double theta_hat = state[1];
            const double theta_tilde = p.theta_star - theta_hat;
            const double phi = regressor(t, p);
            const double e_dot = -p.a * e + phi * theta_tilde;
            const double v = 0.5 * e * e + 0.5 * theta_tilde * theta_tilde / p.gamma;
            const double v_dot = -p.a * e * e;

            max_abs_e_dot = std::max(max_abs_e_dot, std::abs(e_dot));
            csv << t << ',' << e << ',' << theta_hat << ',' << theta_tilde << ','
                << phi << ',' << e_dot << ',' << v << ',' << v_dot << ','
                << integral_e2 << '\n';

            if (k == steps) {
                std::cout << std::scientific << std::setprecision(6)
                          << "Final tracking error: " << e << '\n'
                          << "Final parameter error: " << theta_tilde << '\n'
                          << std::fixed << std::setprecision(6)
                          << "Approximate integral of e(t)^2: " << integral_e2 << '\n'
                          << "Maximum absolute e_dot: " << max_abs_e_dot << '\n'
                          << std::scientific
                          << "Maximum numerical increase in V: " << max_v_increase << '\n'
                          << std::boolalpha
                          << "V is numerically nonincreasing: " << (max_v_increase <= 1.0e-10) << '\n';
                break;
            }

            const State next_state = rk4_step(t, state, dt, p);
            const double next_e2 = next_state[0] * next_state[0];
            integral_e2 += 0.5 * (previous_e2 + next_e2) * dt;
            previous_e2 = next_e2;

            const double next_theta_tilde = p.theta_star - next_state[1];
            const double next_v = 0.5 * next_e2
                                + 0.5 * next_theta_tilde * next_theta_tilde / p.gamma;
            max_v_increase = std::max(max_v_increase, next_v - previous_v);
            previous_v = next_v;
            state = next_state;
        }
    } catch (const std::exception& ex) {
        std::cerr << "Error: " << ex.what() << '\n';
        return 1;
    }
    return 0;
}

15. Java Implementation

The Java implementation uses standard Java I/O and numerical types. Its equations and RK4 stages match the Python and C++ versions.

Chapter3_Lesson5.java

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;

/**
 * Chapter 3, Lesson 5: Barbalat's Lemma and convergence.
 * Standard-library RK4 simulation of a scalar adaptive error system.
 */
public final class Chapter3_Lesson5 {
    private Chapter3_Lesson5() {}

    private static final class Parameters {
        final double a = 1.0;
        final double gamma = 2.0;
        final double thetaStar = 2.5;
        final double regressorDecay = 0.20;
    }

    private static double regressor(double t, Parameters p) {
        return Math.exp(-p.regressorDecay * t);
    }

    private static double[] dynamics(double t, double[] state, Parameters p) {
        double e = state[0];
        double thetaHat = state[1];
        double thetaTilde = p.thetaStar - thetaHat;
        double phi = regressor(t, p);
        return new double[] {
            -p.a * e + phi * thetaTilde,
            p.gamma * phi * e
        };
    }

    private static double[] addScaled(double[] x, double[] k, double scale) {
        return new double[] {x[0] + scale * k[0], x[1] + scale * k[1]};
    }

    private static double[] rk4Step(double t, double[] x, double dt, Parameters p) {
        double[] k1 = dynamics(t, x, p);
        double[] k2 = dynamics(t + 0.5 * dt, addScaled(x, k1, 0.5 * dt), p);
        double[] k3 = dynamics(t + 0.5 * dt, addScaled(x, k2, 0.5 * dt), p);
        double[] k4 = dynamics(t + dt, addScaled(x, k3, dt), p);

        return new double[] {
            x[0] + (dt / 6.0) * (k1[0] + 2.0 * k2[0] + 2.0 * k3[0] + k4[0]),
            x[1] + (dt / 6.0) * (k1[1] + 2.0 * k2[1] + 2.0 * k3[1] + k4[1])
        };
    }

    public static void main(String[] args) {
        Locale.setDefault(Locale.US);
        Parameters p = new Parameters();
        double tFinal = 40.0;
        double dt = 0.002;
        if (tFinal <= 0.0 || dt <= 0.0) {
            throw new IllegalArgumentException("tFinal and dt must be positive");
        }

        double[] state = {1.5, -1.0};
        double integralE2 = 0.0;
        double previousE2 = state[0] * state[0];
        double previousThetaTilde = p.thetaStar - state[1];
        double previousV = 0.5 * previousE2
                         + 0.5 * previousThetaTilde * previousThetaTilde / p.gamma;
        double maxVIncrease = -Double.MAX_VALUE;
        double maxAbsEDot = 0.0;
        long steps = Math.round(tFinal / dt);

        Path output = Path.of("Chapter3_Lesson5_results_java.csv");
        try (BufferedWriter writer = Files.newBufferedWriter(
                output, StandardCharsets.UTF_8)) {
            writer.write("time,error,theta_hat,theta_tilde,phi,error_dot,lyapunov,lyapunov_dot,integral_error_sq\n");

            for (long k = 0; k <= steps; k++) {
                double t = k * dt;
                double e = state[0];
                double thetaHat = state[1];
                double thetaTilde = p.thetaStar - thetaHat;
                double phi = regressor(t, p);
                double eDot = -p.a * e + phi * thetaTilde;
                double v = 0.5 * e * e + 0.5 * thetaTilde * thetaTilde / p.gamma;
                double vDot = -p.a * e * e;
                maxAbsEDot = Math.max(maxAbsEDot, Math.abs(eDot));

                writer.write(String.format(Locale.US,
                    "%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f%n",
                    t, e, thetaHat, thetaTilde, phi, eDot, v, vDot, integralE2));

                if (k == steps) {
                    System.out.printf(Locale.US, "Final tracking error: % .6e%n", e);
                    System.out.printf(Locale.US, "Final parameter error: % .6e%n", thetaTilde);
                    System.out.printf(Locale.US, "Approximate integral of e(t)^2: %.6f%n", integralE2);
                    System.out.printf(Locale.US, "Maximum absolute e_dot: %.6f%n", maxAbsEDot);
                    System.out.printf(Locale.US, "Maximum numerical increase in V: %.3e%n", maxVIncrease);
                    System.out.println("V is numerically nonincreasing: " + (maxVIncrease <= 1.0e-10));
                    break;
                }

                double[] nextState = rk4Step(t, state, dt, p);
                double nextE2 = nextState[0] * nextState[0];
                integralE2 += 0.5 * (previousE2 + nextE2) * dt;
                previousE2 = nextE2;

                double nextThetaTilde = p.thetaStar - nextState[1];
                double nextV = 0.5 * nextE2
                             + 0.5 * nextThetaTilde * nextThetaTilde / p.gamma;
                maxVIncrease = Math.max(maxVIncrease, nextV - previousV);
                previousV = nextV;
                state = nextState;
            }
        } catch (IOException ex) {
            throw new RuntimeException("Cannot write output CSV", ex);
        }
    }
}

16. MATLAB and Simulink Implementation

The MATLAB script uses ode45, computes the Lyapunov and integral diagnostics, and exports a table. When Simulink is installed, the same script programmatically creates Chapter3_Lesson5_Simulink.slx from standard Clock, Gain, Product, Sum, and Integrator blocks.

Chapter3_Lesson5.m

%% Chapter3_Lesson5.m
% Barbalat's Lemma and convergence for a scalar adaptive error system.
% The script runs an ODE45 simulation and, when Simulink is available,
% programmatically creates an equivalent block diagram.

clear; close all; clc;

p.a = 1.0;
p.gamma = 2.0;
p.thetaStar = 2.5;
p.regressorDecay = 0.20;

x0 = [1.5; -1.0];                 % [e(0); thetaHat(0)]
tSpan = linspace(0.0, 40.0, 4001);

options = odeset('RelTol', 1e-9, 'AbsTol', 1e-11);
[t, x] = ode45(@(time, state) adaptiveDynamics(time, state, p), ...
               tSpan, x0, options);

e = x(:, 1);
thetaHat = x(:, 2);
thetaTilde = p.thetaStar - thetaHat;
phi = exp(-p.regressorDecay .* t);
eDot = -p.a .* e + phi .* thetaTilde;
V = 0.5 .* e.^2 + 0.5 .* thetaTilde.^2 ./ p.gamma;
VDot = -p.a .* e.^2;
integralE2 = cumtrapz(t, e.^2);

fprintf('Final tracking error: % .6e\n', e(end));
fprintf('Final parameter error: % .6e\n', thetaTilde(end));
fprintf('Approximate integral of e(t)^2: %.6f\n', integralE2(end));
fprintf('Maximum absolute e_dot: %.6f\n', max(abs(eDot)));
fprintf('Maximum numerical increase in V: %.3e\n', max(diff(V)));
fprintf('V is numerically nonincreasing: %d\n', max(diff(V)) <= 1e-8);

results = table(t, e, thetaHat, thetaTilde, phi, eDot, V, VDot, integralE2);
writetable(results, 'Chapter3_Lesson5_results_matlab.csv');

figure('Name', 'Adaptive states');
plot(t, e, 'LineWidth', 1.4); hold on;
plot(t, thetaTilde, 'LineWidth', 1.4);
grid on;
xlabel('time');
ylabel('signal value');
legend('tracking error e(t)', 'parameter error thetaTilde(t)', ...
       'Location', 'best');
title('Tracking convergence without guaranteed parameter convergence');

figure('Name', 'Barbalat diagnostics');
plot(t, V, 'LineWidth', 1.4); hold on;
plot(t, integralE2, 'LineWidth', 1.4);
grid on;
xlabel('time');
ylabel('diagnostic value');
legend('V(t)', 'integral_0^t e(tau)^2 d tau', 'Location', 'best');
title('Lyapunov monotonicity and square integrability');

if license('test', 'Simulink') && exist('simulink', 'file') == 2
    buildBarbalatSimulinkModel(p, x0);
    fprintf('Created Chapter3_Lesson5_Simulink.slx\n');
else
    fprintf('Simulink is unavailable; skipped model generation.\n');
end

function dx = adaptiveDynamics(time, state, p)
    e = state(1);
    thetaHat = state(2);
    thetaTilde = p.thetaStar - thetaHat;
    phi = exp(-p.regressorDecay * time);

    eDot = -p.a * e + phi * thetaTilde;
    thetaHatDot = p.gamma * phi * e;
    dx = [eDot; thetaHatDot];
end

function buildBarbalatSimulinkModel(p, x0)
    model = 'Chapter3_Lesson5_Simulink';
    if bdIsLoaded(model)
        close_system(model, 0);
    end
    if exist([model '.slx'], 'file')
        delete([model '.slx']);
    end

    new_system(model);
    load_system('simulink');

    add_block('simulink/Sources/Clock', [model '/Clock'], ...
        'Position', [35 35 65 65]);
    add_block('simulink/Math Operations/Gain', [model '/DecayGain'], ...
        'Gain', num2str(-p.regressorDecay, 16), ...
        'Position', [100 30 165 70]);
    add_block('simulink/Math Operations/Math Function', [model '/Exp'], ...
        'Operator', 'exp', 'Position', [205 30 255 70]);

    add_block('simulink/Sources/Constant', [model '/ThetaStar'], ...
        'Value', num2str(p.thetaStar, 16), ...
        'Position', [35 145 90 175]);
    add_block('simulink/Math Operations/Sum', [model '/ThetaError'], ...
        'Inputs', '+-', 'Position', [150 130 180 190]);
    add_block('simulink/Math Operations/Product', [model '/PhiTheta'], ...
        'Position', [300 120 335 165]);

    add_block('simulink/Math Operations/Gain', [model '/MinusA'], ...
        'Gain', num2str(-p.a, 16), ...
        'Position', [300 225 365 265]);
    add_block('simulink/Math Operations/Sum', [model '/ErrorDerivative'], ...
        'Inputs', '++', 'Position', [420 155 450 215]);
    add_block('simulink/Continuous/Integrator', [model '/Error'], ...
        'InitialCondition', num2str(x0(1), 16), ...
        'Position', [500 160 530 210]);

    add_block('simulink/Math Operations/Product', [model '/PhiError'], ...
        'Position', [300 325 335 370]);
    add_block('simulink/Math Operations/Gain', [model '/Gamma'], ...
        'Gain', num2str(p.gamma, 16), ...
        'Position', [390 325 455 365]);
    add_block('simulink/Continuous/Integrator', [model '/ThetaHat'], ...
        'InitialCondition', num2str(x0(2), 16), ...
        'Position', [500 320 530 370]);

    add_block('simulink/Signal Routing/Mux', [model '/Mux'], ...
        'Inputs', '2', 'Position', [610 210 615 290]);
    add_block('simulink/Sinks/To Workspace', [model '/ToWorkspace'], ...
        'VariableName', 'barbalatSimulinkSignals', ...
        'SaveFormat', 'Structure With Time', ...
        'Position', [675 225 775 275]);

    add_line(model, 'Clock/1', 'DecayGain/1', 'autorouting', 'on');
    add_line(model, 'DecayGain/1', 'Exp/1', 'autorouting', 'on');
    add_line(model, 'ThetaStar/1', 'ThetaError/1', 'autorouting', 'on');
    add_line(model, 'ThetaHat/1', 'ThetaError/2', 'autorouting', 'on');
    add_line(model, 'Exp/1', 'PhiTheta/1', 'autorouting', 'on');
    add_line(model, 'ThetaError/1', 'PhiTheta/2', 'autorouting', 'on');
    add_line(model, 'Error/1', 'MinusA/1', 'autorouting', 'on');
    add_line(model, 'MinusA/1', 'ErrorDerivative/1', 'autorouting', 'on');
    add_line(model, 'PhiTheta/1', 'ErrorDerivative/2', 'autorouting', 'on');
    add_line(model, 'ErrorDerivative/1', 'Error/1', 'autorouting', 'on');
    add_line(model, 'Exp/1', 'PhiError/1', 'autorouting', 'on');
    add_line(model, 'Error/1', 'PhiError/2', 'autorouting', 'on');
    add_line(model, 'PhiError/1', 'Gamma/1', 'autorouting', 'on');
    add_line(model, 'Gamma/1', 'ThetaHat/1', 'autorouting', 'on');
    add_line(model, 'Error/1', 'Mux/1', 'autorouting', 'on');
    add_line(model, 'ThetaHat/1', 'Mux/2', 'autorouting', 'on');
    add_line(model, 'Mux/1', 'ToWorkspace/1', 'autorouting', 'on');

    set_param(model, 'StopTime', '40', 'Solver', 'ode45');
    save_system(model);
    close_system(model, 0);
end

17. Wolfram Mathematica Implementation

The notebook uses NDSolveValue for integration, NIntegrate for the finite-energy diagnostic, and NMaxValue for a derivative bound over the simulated horizon.

Chapter3_Lesson5.nb

ClearAll["Global`*"]

a = 1.0;
gamma = 2.0;
thetaStar = 2.5;
lambda = 0.20;
tFinal = 40.0;

phi[t_] := Exp[-lambda t];

solution = NDSolveValue[
  {
    e'[t] == -a e[t] + phi[t] (thetaStar - thetaHat[t]),
    thetaHat'[t] == gamma phi[t] e[t],
    e[0] == 1.5,
    thetaHat[0] == -1.0
  },
  {e, thetaHat},
  {t, 0, tFinal},
  Method -> {
    "TimeIntegration" -> {
      "ExplicitRungeKutta",
      "DifferenceOrder" -> 4
    }
  }
];

eFun[t_] := solution[[1]][t];
thetaHatFun[t_] := solution[[2]][t];
thetaTildeFun[t_] := thetaStar - thetaHatFun[t];
vFun[t_] := 1/2 eFun[t]^2 + 1/(2 gamma) thetaTildeFun[t]^2;
vDotTheory[t_] := -a eFun[t]^2;
eDotFun[t_] := -a eFun[t] + phi[t] thetaTildeFun[t];

integralE2 = NIntegrate[eFun[t]^2, {t, 0, tFinal}];
finalError = eFun[tFinal];
finalParameterError = thetaTildeFun[tFinal];
maxAbsEDot = NMaxValue[
  {Abs[eDotFun[t]], 0 <= t <= tFinal},
  t
];

Grid[
  {
    {"Final tracking error", ScientificForm[finalError, 6]},
    {"Final parameter error", ScientificForm[finalParameterError, 6]},
    {"Integral of e(t)^2", N[integralE2, 8]},
    {"Maximum absolute e'(t)", N[maxAbsEDot, 8]}
  },
  Frame -> All
]

Plot[
  Evaluate[{eFun[t], thetaTildeFun[t]}],
  {t, 0, tFinal},
  PlotLegends -> {
    "tracking error e(t)",
    "parameter error thetaTilde(t)"
  },
  AxesLabel -> {"time", "signal value"},
  PlotRange -> All,
  ImageSize -> Large
]

Plot[
  Evaluate[{
    vFun[t],
    NIntegrate[eFun[tau]^2, {tau, 0, t}]
  }],
  {t, 0, tFinal},
  PlotLegends -> {
    "V(t)",
    "integral from 0 to t of e(tau)^2"
  },
  AxesLabel -> {"time", "diagnostic value"},
  PlotRange -> All,
  ImageSize -> Large
]

samples = Table[
  {
    time,
    eFun[time],
    thetaHatFun[time],
    thetaTildeFun[time],
    phi[time],
    eDotFun[time],
    vFun[time],
    vDotTheory[time]
  },
  {time, 0, tFinal, 0.01}
];

Export[
  "Chapter3_Lesson5_results_mathematica.csv",
  Prepend[
    samples,
    {
      "time",
      "error",
      "theta_hat",
      "theta_tilde",
      "phi",
      "error_dot",
      "lyapunov",
      "lyapunov_dot"
    }
  ]
];

18. Problems and Solutions

Problem 1 (Uniformly continuous \(L_p\) signal): Let \( f:[0,\infty)\longrightarrow\mathbb{R} \) be uniformly continuous and suppose \( \int_{0}^{\infty}|f(t)|^pdt<\infty \) for some \( p>0 \). Prove that \( f(t)\longrightarrow0 \).

Solution:

Suppose not. Then some \( \varepsilon>0 \) and a sequence \( t_k\longrightarrow\infty \) satisfy \( |f(t_k)|\ge\varepsilon \). Uniform continuity gives a fixed \( \delta>0 \) such that

\[ |t-t_k|<\delta \;\Longrightarrow\; |f(t)|\ge\frac{\varepsilon}{2}. \]

Select a subsequence for which the intervals \( [t_k-\delta/2,t_k+\delta/2] \) are disjoint. Then

\[ \int_{0}^{\infty}|f(t)|^pdt \ge\sum_{k=1}^{\infty} \delta\left(\frac{\varepsilon}{2}\right)^p =\infty, \]

contradicting the finite \( L_p \) norm.

Problem 2 (Full scalar adaptive proof): For

\[ \dot e=-ae+\phi(t)\tilde\theta, \qquad \dot{\hat\theta}=\gamma\phi(t)e, \]

assume \( a>0 \), \( \gamma>0 \), bounded \( \phi \), and constant \( \theta^{\ast} \). Prove boundedness of all adaptive variables and convergence of \( e(t) \) to zero.

Solution:

Choose

\[ V=\frac12e^2+\frac{1}{2\gamma}\tilde\theta^2. \]

Because \( \dot{\tilde\theta}=-\gamma\phi e \), one gets

\[ \dot V=e(-ae+\phi\tilde\theta) +\frac{1}{\gamma}\tilde\theta(-\gamma\phi e) =-ae^2. \]

Hence \( V(t)\le V(0) \), so \( e \) and \( \tilde\theta \) are bounded. Also,

\[ \int_{0}^{\infty}e^2(t)dt\le\frac{V(0)}{a}, \]

so \( e\in L_2 \). Bounded \( e \), \( \phi \), and \( \tilde\theta \) imply bounded \( \dot e \). Barbalat’s corollary therefore gives \( e(t)\longrightarrow0 \).

Problem 3 (Why uniform continuity is necessary): Construct a nonnegative continuous function \( f\in L_1 \) that does not tend to zero.

Solution:

For each positive integer \( k \), place a triangular pulse centered at \( t=k \), with height one and base width \( 2^{-k} \). Choose the supports disjoint. The total area is

\[ \sum_{k=1}^{\infty}\frac12\,2^{-k}=\frac12, \]

so \( f\in L_1 \). However, \( f(k)=1 \) for every \( k \), and therefore \( f(t) \) does not tend to zero. The slopes grow without bound as the pulse widths shrink, so the function is not uniformly continuous.

Problem 4 (Vector-valued Barbalat corollary): Let \( x:[0,\infty)\longrightarrow\mathbb{R}^{n} \) be differentiable with \( x\in L_2 \) and \( \dot x\in L_\infty \). Show that \( x(t)\longrightarrow0 \).

Solution:

Bounded \( \dot x \) makes \( x \) uniformly continuous because

\[ \|x(t_2)-x(t_1)\| \le\int_{t_1}^{t_2}\|\dot x(\tau)\|d\tau \le\|\dot x\|_{L_\infty}|t_2-t_1|. \]

Since \( \int_{0}^{\infty}\|x(t)\|^2dt<\infty \), the uniformly-continuous \( L_2 \) corollary applies to the vector norm and gives \( \|x(t)\|\longrightarrow0 \).

Problem 5 (Tracking without identification): Suppose the regressor in the scalar adaptive model becomes identically zero after some finite time \( T \). Determine the post-\( T \) behavior of the tracking and parameter errors.

Solution:

For \( t\ge T \),

\[ \phi(t)=0, \qquad \dot e=-ae, \qquad \dot{\hat\theta}=0. \]

Therefore,

\[ e(t)=e(T)e^{-a(t-T)}\longrightarrow0, \]

while

\[ \hat\theta(t)=\hat\theta(T), \qquad \tilde\theta(t)=\theta^{\ast}-\hat\theta(T). \]

The parameter error freezes at a generally nonzero value even though the tracking error converges exponentially.

19. Summary

Barbalat’s lemma converts an integral convergence statement into pointwise convergence when uniform continuity excludes arbitrarily narrow persistent spikes. In adaptive control, a Lyapunov law commonly gives bounded state and parameter errors together with \( e\in L_2 \). Bounded closed-loop dynamics then yield \( \dot e\in L_\infty \), so Barbalat implies \( e(t)\longrightarrow0 \). This proves asymptotic tracking, not necessarily convergence of the estimated parameters. The distinction between state convergence and parameter convergence will be developed later through explicit conditions on the regressor.

20. References

  1. Barbălat, I. (1959). Systèmes d’équations différentielles d’oscillations non linéaires. Revue Roumaine de Mathématiques Pures et Appliquées, 4, 267–270.
  2. Tao, G. (1997). A simple alternative to the Barbalat lemma. IEEE Transactions on Automatic Control, 42(5), 698.
  3. Farkas, B., & Wegner, S.-A. (2016). Variations on Barbalat’s lemma. The American Mathematical Monthly, 123(8), 825–830.
  4. Anfinsen, H., & Aamo, O. M. (2018). A note on establishing convergence in adaptive systems. Automatica, 93, 545–549.
  5. Karafyllis, I., & Chaillet, A. (2021). Lyapunov conditions for uniform asymptotic output stability and a relaxation of Barbălat’s lemma. Automatica, 132, 109792.
Support CaaT Academy

Help keep these engineering tutorials free and growing

If these lessons, examples, and project pages help you, a small donation supports the continued creation and improvement of free control, robotics, software, and engineering education resources.

Created and maintained by Abolfazl Mohammadijoo.