Chapter 11: Indirect Adaptive Control and Self-Tuning Regulators (STR)

Lesson 1: Indirect vs Direct Adaptive Control – Conceptual Distinction

This lesson establishes the structural, mathematical, and implementation-level distinction between direct and indirect adaptive control. Both approaches alter a controller online, but they adapt different parameter objects. A direct method updates controller parameters from a control-relevant error, whereas an indirect method first estimates a plant model and then redesigns the controller from that estimate. A common first-order example is used so that the distinction is not obscured by high-order realization details.

1. Learning Objectives and Scope

After completing this lesson, a student should be able to:

  • distinguish the parameter vector of the plant from the parameter vector of the controller;
  • identify whether an adaptive architecture is direct or indirect from its signal flow and update law;
  • derive a first-order direct adaptive law using a Lyapunov function;
  • derive a prediction-error estimator used in an indirect architecture;
  • explain certainty equivalence and the conditions under which it is safe to use;
  • recognize that tracking convergence and plant-parameter convergence are different claims.

Recursive least squares is intentionally not developed here. The indirect example uses a continuous-time gradient predictor built from concepts already introduced in earlier chapters. Recursive least squares is introduced in Chapter 12.

2. The Distinction in One Sentence

Let \( \boldsymbol{\theta}_p \) denote unknown plant parameters and let \( \boldsymbol{\theta}_c \) denote adjustable controller parameters.

\[ \begin{aligned} \text{Direct adaptation:}\quad & \text{error signals} \longmapsto \dot{\hat{\boldsymbol{\theta} } }_c \longmapsto u, \\ \text{Indirect adaptation:}\quad & \text{input-output data} \longmapsto \dot{\hat{\boldsymbol{\theta} } }_p \longmapsto \hat{\boldsymbol{\theta} }_c = \mathcal{G}\!\left(\hat{\boldsymbol{\theta} }_p\right) \longmapsto u . \end{aligned} \]

Therefore, the defining question is not whether parameters are adapted; both architectures adapt parameters. The defining question is which parameters are estimated explicitly.

flowchart LR
  subgraph D["Direct adaptive control"]
    DR["Reference r"] --> DC["Adaptive controller"]
    DY["Plant output y"] --> DE["Tracking error"]
    DM["Reference-model output ym"] --> DE
    DE --> DA["Update controller gains"]
    DA --> DC
    DC --> DP["Plant"]
    DP --> DY
  end

  subgraph I["Indirect adaptive control"]
    IR["Reference r"] --> IC["Controller computed from estimated plant"]
    IY["Plant output y"] --> ID["Online plant identifier"]
    IU["Applied input u"] --> ID
    ID --> IP["Estimated plant parameters"]
    IP --> IS["Controller synthesis map"]
    IS --> IC
    IC --> IPL["Plant"]
    IPL --> IY
    IC --> IU
  end
        

3. Common First-Order Plant and Reference Model

Consider the uncertain scalar plant

\[ \dot{y}(t)=-a\,y(t)+b\,u(t), \qquad a>0, \qquad b\neq 0 . \]

The numerical value of \( b \) is unknown, but its sign is assumed known. This is the standard sign-definite case already encountered in model-reference adaptive control. The desired response is specified by

\[ \dot{y}_m(t)=-a_m y_m(t)+b_m r(t), \qquad a_m>0 . \]

Define the tracking error \( e(t)=y(t)-y_m(t) \). If the plant parameters were known, a static model-matching controller could be selected as

\[ u(t)=k_y^\star y(t)+k_r^\star r(t), \qquad k_y^\star=\frac{a-a_m}{b}, \qquad k_r^\star=\frac{b_m}{b}. \]

Substitution gives the desired closed-loop dynamics:

\[ \begin{aligned} \dot{y} &=-a y+b\left(k_y^\star y+k_r^\star r\right) \\ &=-a y+(a-a_m)y+b_m r \\ &=-a_m y+b_m r . \end{aligned} \]

Consequently,

\[ \dot{e}=-a_m e, \qquad e(t)=e(0)\exp(-a_m t). \]

Both adaptive architectures attempt to recover this behavior without knowing \( a \) and \( b \), but they take different routes.

4. Direct Adaptive Control

4.1 Controller parameterization

Direct adaptation treats the controller gains themselves as the adjustable parameter vector:

\[ u = \hat{k}_y y+\hat{k}_r r = \hat{\boldsymbol{\theta} }_c^{T}\boldsymbol{\phi}, \qquad \hat{\boldsymbol{\theta} }_c = \begin{bmatrix} \hat{k}_y \\ \hat{k}_r \end{bmatrix}, \qquad \boldsymbol{\phi} = \begin{bmatrix} y \\ r \end{bmatrix}. \]

No explicit estimates \( \hat a \) or \( \hat b \) are maintained. Define

\[ \tilde{\boldsymbol{\theta} }_c = \hat{\boldsymbol{\theta} }_c-\boldsymbol{\theta}_c^\star, \qquad \boldsymbol{\theta}_c^\star = \begin{bmatrix} (a-a_m)/b \\ b_m/b \end{bmatrix}. \]

4.2 Tracking-error dynamics

Add and subtract the ideal controller contribution. The result is

\[ \dot{e} = -a_m e + b\, \tilde{\boldsymbol{\theta} }_c^{T}\boldsymbol{\phi}. \]

4.3 Lyapunov derivation

Choose

\[ V = \frac{1}{2}e^2 + \frac{|b|}{2\gamma} \tilde{\boldsymbol{\theta} }_c^{T} \tilde{\boldsymbol{\theta} }_c, \qquad \gamma>0 . \]

Because the ideal gains are constant,

\[ \dot{V} = -a_m e^2 + b e\, \tilde{\boldsymbol{\theta} }_c^{T}\boldsymbol{\phi} + \frac{|b|}{\gamma} \tilde{\boldsymbol{\theta} }_c^{T} \dot{\hat{\boldsymbol{\theta} } }_c . \]

Select the direct adaptive law

\[ \boxed{ \dot{\hat{\boldsymbol{\theta} } }_c = -\gamma\,\operatorname{sgn}(b)\, \boldsymbol{\phi}\,e } \]

or, componentwise,

\[ \dot{\hat{k} }_y = -\gamma\,\operatorname{sgn}(b)\,y e, \qquad \dot{\hat{k} }_r = -\gamma\,\operatorname{sgn}(b)\,r e . \]

Since \( |b|\operatorname{sgn}(b)=b \), the cross terms cancel:

\[ \dot{V}=-a_m e^2\leq 0 .\]

Thus \( e \) and \( \tilde{\boldsymbol{\theta} }_c \) are bounded, and \( e\in\mathcal{L}_2 \). Under the standard boundedness conditions that make \( \dot e \) bounded, Barbalat's lemma yields \( e(t)\longrightarrow 0 \). This conclusion concerns tracking. It does not imply \( \hat{\boldsymbol{\theta} }_c\longrightarrow \boldsymbol{\theta}_c^\star \) unless an adequate excitation condition is also satisfied.

5. Indirect Adaptive Control

5.1 Explicit plant estimation

Indirect adaptation maintains a plant model. For the same plant, consider the stable output predictor

\[ \dot{\hat{y} } = -\hat{a}\,y + \hat{b}\,u + \ell\left(y-\hat{y}\right), \qquad \ell>0 . \]

Define the prediction error and plant-parameter errors as

\[ \varepsilon=y-\hat{y}, \qquad \tilde a=\hat a-a, \qquad \tilde b=\hat b-b . \]

The prediction-error dynamics are

\[ \begin{aligned} \dot{\varepsilon} &= \dot y-\dot{\hat y} \\ &= -\ell\varepsilon+\tilde a\,y-\tilde b\,u . \end{aligned} \]

5.2 Gradient identifier

Choose the estimator Lyapunov function

\[ V_I = \frac{1}{2}\varepsilon^2 + \frac{1}{2\gamma_a}\tilde a^2 + \frac{1}{2\gamma_b}\tilde b^2, \qquad \gamma_a>0, \qquad \gamma_b>0 . \]

Its derivative is

\[ \dot V_I = -\ell\varepsilon^2 + \tilde a \left( y\varepsilon+\frac{1}{\gamma_a}\dot{\hat a} \right) + \tilde b \left( -u\varepsilon+\frac{1}{\gamma_b}\dot{\hat b} \right). \]

Select

\[ \boxed{ \dot{\hat a}=-\gamma_a y\varepsilon, \qquad \dot{\hat b}=\gamma_b u\varepsilon } \]

to obtain

\[ \dot V_I=-\ell\varepsilon^2\leq 0 . \]

This establishes bounded prediction error and bounded estimates for the idealized predictor. Parameter convergence still requires excitation. The estimator proof alone is not a proof that the complete adaptive feedback loop is globally stable, because the controller is recomputed from the estimates and therefore changes the signals used by the identifier.

5.3 Certainty-equivalent controller redesign

The indirect controller substitutes the estimated plant parameters into the known-plant design formula:

\[ \hat{k}_y = \frac{\hat a-a_m}{\hat b}, \qquad \hat{k}_r = \frac{b_m}{\hat b}, \qquad u=\hat{k}_y y+\hat{k}_r r . \]

This operation is called certainty equivalence: the estimated plant is treated as if it were the true plant during controller synthesis. Projection is used in the implementations to keep \( \hat b \) away from zero:

\[ 0<b_{\min}\leq\hat b(t)\leq b_{\max}. \]

If \( \hat a(t)\longrightarrow a \) and \( \hat b(t)\longrightarrow b \), with \( |b|\geq b_{\min} \), then continuity of the synthesis map gives

\[ \hat{k}_y(t)\longrightarrow k_y^\star, \qquad \hat{k}_r(t)\longrightarrow k_r^\star . \]

Under these conditions, the closed-loop system approaches the desired model dynamics. This is a conditional implication; it must not be confused with an unconditional proof of parameter convergence.

6. Parameter Spaces and the Synthesis Map

The direct and indirect approaches operate in different parameter spaces. In this example,

\[ \boldsymbol{\theta}_p = \begin{bmatrix} a \\ b \end{bmatrix}, \qquad \boldsymbol{\theta}_c = \begin{bmatrix} k_y \\ k_r \end{bmatrix}, \qquad \boldsymbol{\theta}_c = \mathcal{G}(\boldsymbol{\theta}_p) = \begin{bmatrix} (a-a_m)/b \\ b_m/b \end{bmatrix}. \]

flowchart TD
  E["Measured data: reference, input, output"] --> Q{"Which parameter \nobject is updated?"}
  Q -->|"Controller gains"| D["Direct method"]
  Q -->|"Plant model"| I["Indirect method"]
  D --> DG["Updated gains enter control law immediately"]
  I --> PE["Estimate plant parameters"]
  PE --> SM["Apply controller-synthesis map"]
  SM --> IG["Computed gains enter control law"]
  DG --> O["Closed-loop tracking"]
  IG --> O
        

The Jacobian of the synthesis map quantifies how plant-estimation errors are amplified into controller-gain errors:

\[ \frac{\partial\mathcal{G} }{\partial\boldsymbol{\theta}_p} = \begin{bmatrix} 1/b & -(a-a_m)/b^2 \\ 0 & -b_m/b^2 \end{bmatrix}. \]

Therefore, indirect redesign becomes highly sensitive when the estimated input gain approaches zero. This is a structural reason for projection, sign knowledge, and supervisory safeguards. A direct parameterization may avoid explicit division by \( \hat b \), although it has its own robustness and identifiability limitations.

7. Detailed Comparison

Property Direct Adaptive Control Indirect Adaptive Control
Adjusted object Controller parameters Plant parameters, followed by controller redesign
Explicit plant model Not required as an online estimate Central part of the architecture
Main error used Usually tracking or augmented error Usually prediction or identification error
Controller update Adaptive law directly changes gains Synthesis map converts plant estimates into gains
Interpretability Gains are directly visible, but may not identify the plant Estimated physical/model parameters can be inspected
Singularity risk Depends on controller parameterization Can arise when synthesis divides by an uncertain estimate
Modularity Controller and adaptation proof are tightly coupled Identifier and controller design can be modular, but their feedback coupling remains important
Tracking without parameter convergence Possible Possible, depending on equivalent models and closed-loop excitation
Typical classical form Direct MRAC or direct STR Identifier plus certainty-equivalent STR

7.1 A terminology warning

A controller is not classified as indirect merely because it contains an estimator, observer, or filtered signal. It is indirect when an explicit estimate of the plant model is used in a controller-synthesis map. Similarly, a direct method can contain dynamic filters and auxiliary states while still adapting controller parameters directly.

8. Numerical Experiment

The implementations use

\[ a=1.2, \qquad b=0.8, \qquad a_m=2, \qquad b_m=2. \]

The ideal controller gains are therefore

\[ k_y^\star=-1, \qquad k_r^\star=2.5. \]

The reference contains two constant intervals followed by a two-frequency signal. This provides a more informative comparison than a single constant command. The direct implementation starts with zero controller gains. The indirect implementation starts from \( \hat a(0)=0.6 \) and \( \hat b(0)=1.4 \), with projection intervals \( \hat a\in[0.1,3] \) and \( \hat b\in[0.2,3] \).

With an explicit-Euler step of \( 10^{-3} \) seconds, the supplied implementations produce approximately the following values:

Quantity Approximate value at 20 s
Direct final \( \hat k_y \) -0.5777
Direct final \( \hat k_r \) 2.1431
Indirect final \( \hat a \) 1.0698
Indirect final \( \hat b \) 0.7521
Direct tracking RMSE after 10 s 0.0754
Indirect tracking RMSE after 10 s 0.0276

These numbers illustrate one experiment only; they do not establish that indirect adaptation is generally superior. The result changes with excitation, adaptation gains, noise, projection bounds, numerical integration, and initial conditions. The important observation is that the two algorithms evolve different parameter objects.

9. Python Implementation

The Python implementation uses NumPy for arrays and Matplotlib for visualization. The adaptive laws themselves are implemented from scratch.

Chapter11_Lesson1.py

"""
Chapter11_Lesson1.py
Direct versus indirect adaptive control for a first-order plant.

Plant:
    y_dot = -a*y + b*u

Reference model:
    ym_dot = -am*ym + bm*r

The direct controller adapts controller gains (ky_hat, kr_hat).
The indirect controller estimates plant parameters (a_hat, b_hat) and
computes controller gains through certainty equivalence.
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import Callable

import matplotlib.pyplot as plt
import numpy as np


@dataclass
class SimulationResult:
    time: np.ndarray
    reference: np.ndarray
    output: np.ndarray
    model_output: np.ndarray
    control: np.ndarray
    parameter_1: np.ndarray
    parameter_2: np.ndarray
    label_1: str
    label_2: str


def reference_signal(t: float) -> float:
    """A bounded, sufficiently varied reference for comparison."""
    if t < 3.0:
        return 1.0
    if t < 6.0:
        return -0.5
    return 0.8 * math.sin(0.7 * t) + 0.5 * math.sin(1.9 * t)


def clamp(value: float, lower: float, upper: float) -> float:
    return min(max(value, lower), upper)


def simulate_direct(
    a: float,
    b: float,
    am: float,
    bm: float,
    gamma: float,
    dt: float,
    duration: float,
) -> SimulationResult:
    """Direct adaptation: update controller gains without estimating a and b."""
    steps = int(duration / dt) + 1
    time = np.linspace(0.0, duration, steps)
    reference = np.zeros(steps)
    output = np.zeros(steps)
    model_output = np.zeros(steps)
    control = np.zeros(steps)
    ky_history = np.zeros(steps)
    kr_history = np.zeros(steps)

    y = 0.0
    ym = 0.0
    ky_hat = 0.0
    kr_hat = 0.0
    sign_b = 1.0 if b > 0.0 else -1.0

    for k, t in enumerate(time):
        r = reference_signal(float(t))
        u = ky_hat * y + kr_hat * r
        e = y - ym

        y_dot = -a * y + b * u
        ym_dot = -am * ym + bm * r
        ky_dot = -gamma * sign_b * y * e
        kr_dot = -gamma * sign_b * r * e

        reference[k] = r
        output[k] = y
        model_output[k] = ym
        control[k] = u
        ky_history[k] = ky_hat
        kr_history[k] = kr_hat

        y += dt * y_dot
        ym += dt * ym_dot
        ky_hat += dt * ky_dot
        kr_hat += dt * kr_dot

    return SimulationResult(
        time,
        reference,
        output,
        model_output,
        control,
        ky_history,
        kr_history,
        "ky_hat",
        "kr_hat",
    )


def simulate_indirect(
    a: float,
    b: float,
    am: float,
    bm: float,
    gamma_a: float,
    gamma_b: float,
    observer_gain: float,
    dt: float,
    duration: float,
) -> SimulationResult:
    """
    Indirect adaptation: estimate plant parameters and redesign the controller.

    Predictor:
        yhat_dot = -a_hat*y + b_hat*u + observer_gain*(y-yhat)

    Gradient estimator:
        a_hat_dot = -gamma_a*y*(y-yhat)
        b_hat_dot =  gamma_b*u*(y-yhat)
    """
    steps = int(duration / dt) + 1
    time = np.linspace(0.0, duration, steps)
    reference = np.zeros(steps)
    output = np.zeros(steps)
    model_output = np.zeros(steps)
    control = np.zeros(steps)
    a_history = np.zeros(steps)
    b_history = np.zeros(steps)

    y = 0.0
    ym = 0.0
    y_hat = 0.0
    a_hat = 0.6
    b_hat = 1.4

    a_bounds = (0.1, 3.0)
    b_bounds = (0.2, 3.0)  # known positive control direction

    for k, t in enumerate(time):
        r = reference_signal(float(t))
        b_safe = clamp(b_hat, *b_bounds)

        ky_hat = (a_hat - am) / b_safe
        kr_hat = bm / b_safe
        u = ky_hat * y + kr_hat * r

        prediction_error = y - y_hat
        y_dot = -a * y + b * u
        ym_dot = -am * ym + bm * r
        y_hat_dot = (
            -a_hat * y
            + b_hat * u
            + observer_gain * prediction_error
        )
        a_hat_dot = -gamma_a * y * prediction_error
        b_hat_dot = gamma_b * u * prediction_error

        reference[k] = r
        output[k] = y
        model_output[k] = ym
        control[k] = u
        a_history[k] = a_hat
        b_history[k] = b_hat

        y += dt * y_dot
        ym += dt * ym_dot
        y_hat += dt * y_hat_dot
        a_hat = clamp(a_hat + dt * a_hat_dot, *a_bounds)
        b_hat = clamp(b_hat + dt * b_hat_dot, *b_bounds)

    return SimulationResult(
        time,
        reference,
        output,
        model_output,
        control,
        a_history,
        b_history,
        "a_hat",
        "b_hat",
    )


def rmse_after(result: SimulationResult, start_time: float) -> float:
    mask = result.time >= start_time
    error = result.output[mask] - result.model_output[mask]
    return float(np.sqrt(np.mean(error * error)))


def plot_results(
    direct: SimulationResult,
    indirect: SimulationResult,
    a_true: float,
    b_true: float,
    am: float,
    bm: float,
) -> None:
    ideal_ky = (a_true - am) / b_true
    ideal_kr = bm / b_true

    plt.figure(figsize=(10, 5))
    plt.plot(direct.time, direct.model_output, "--", label="reference model")
    plt.plot(direct.time, direct.output, label="direct output")
    plt.plot(indirect.time, indirect.output, label="indirect output")
    plt.xlabel("Time (s)")
    plt.ylabel("Output")
    plt.title("Direct and indirect adaptive tracking")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()

    plt.figure(figsize=(10, 5))
    plt.plot(direct.time, direct.parameter_1, label="direct ky_hat")
    plt.plot(direct.time, direct.parameter_2, label="direct kr_hat")
    plt.axhline(ideal_ky, linestyle="--", label="ideal ky")
    plt.axhline(ideal_kr, linestyle="--", label="ideal kr")
    plt.xlabel("Time (s)")
    plt.ylabel("Controller parameters")
    plt.title("Direct adaptation")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()

    plt.figure(figsize=(10, 5))
    plt.plot(indirect.time, indirect.parameter_1, label="indirect a_hat")
    plt.plot(indirect.time, indirect.parameter_2, label="indirect b_hat")
    plt.axhline(a_true, linestyle="--", label="true a")
    plt.axhline(b_true, linestyle="--", label="true b")
    plt.xlabel("Time (s)")
    plt.ylabel("Plant-parameter estimates")
    plt.title("Indirect adaptation")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()

    plt.show()


def main() -> None:
    a_true = 1.2
    b_true = 0.8
    am = 2.0
    bm = 2.0
    dt = 0.001
    duration = 20.0

    direct = simulate_direct(
        a=a_true,
        b=b_true,
        am=am,
        bm=bm,
        gamma=2.0,
        dt=dt,
        duration=duration,
    )
    indirect = simulate_indirect(
        a=a_true,
        b=b_true,
        am=am,
        bm=bm,
        gamma_a=1.5,
        gamma_b=1.5,
        observer_gain=4.0,
        dt=dt,
        duration=duration,
    )

    ideal_ky = (a_true - am) / b_true
    ideal_kr = bm / b_true

    print(f"Ideal controller gains: ky={ideal_ky:.4f}, kr={ideal_kr:.4f}")
    print(
        "Direct final gains: "
        f"ky_hat={direct.parameter_1[-1]:.4f}, "
        f"kr_hat={direct.parameter_2[-1]:.4f}"
    )
    print(
        "Indirect final plant estimates: "
        f"a_hat={indirect.parameter_1[-1]:.4f}, "
        f"b_hat={indirect.parameter_2[-1]:.4f}"
    )
    print(f"Direct tracking RMSE after 10 s: {rmse_after(direct, 10.0):.6f}")
    print(f"Indirect tracking RMSE after 10 s: {rmse_after(indirect, 10.0):.6f}")

    plot_results(direct, indirect, a_true, b_true, am, bm)


if __name__ == "__main__":
    main()

10. C++ Implementation

The C++17 implementation uses only the standard library and writes CSV files for plotting. For higher-order and MIMO implementations, a matrix library such as Eigen is commonly useful, but it is unnecessary for this scalar derivation.

Chapter11_Lesson1.cpp

// Chapter11_Lesson1.cpp
// Direct versus indirect adaptive control for a first-order plant.
// Compile: g++ -std=c++17 -O2 Chapter11_Lesson1.cpp -o Chapter11_Lesson1

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

struct Summary {
    double p1{};
    double p2{};
    double rmse_after_10{};
};

double referenceSignal(double t) {
    if (t < 3.0) {
        return 1.0;
    }
    if (t < 6.0) {
        return -0.5;
    }
    return 0.8 * std::sin(0.7 * t) + 0.5 * std::sin(1.9 * t);
}

double clampValue(double value, double lower, double upper) {
    return std::min(std::max(value, lower), upper);
}

Summary simulateDirect(
    double a,
    double b,
    double am,
    double bm,
    double gamma,
    double dt,
    double duration,
    const std::string& csvPath
) {
    std::ofstream csv(csvPath);
    if (!csv) {
        throw std::runtime_error("Cannot open " + csvPath);
    }
    csv << "time,reference,y,ym,u,ky_hat,kr_hat\n";

    double y = 0.0;
    double ym = 0.0;
    double kyHat = 0.0;
    double krHat = 0.0;
    const double signB = (b > 0.0) ? 1.0 : -1.0;
    double squaredErrorSum = 0.0;
    std::size_t rmseCount = 0;

    const int steps = static_cast<int>(duration / dt) + 1;
    for (int k = 0; k < steps; ++k) {
        const double t = k * dt;
        const double r = referenceSignal(t);
        const double u = kyHat * y + krHat * r;
        const double e = y - ym;

        const double yDot = -a * y + b * u;
        const double ymDot = -am * ym + bm * r;
        const double kyDot = -gamma * signB * y * e;
        const double krDot = -gamma * signB * r * e;

        csv << std::setprecision(12)
            << t << ',' << r << ',' << y << ',' << ym << ','
            << u << ',' << kyHat << ',' << krHat << '\n';

        if (t >= 10.0) {
            squaredErrorSum += e * e;
            ++rmseCount;
        }

        y += dt * yDot;
        ym += dt * ymDot;
        kyHat += dt * kyDot;
        krHat += dt * krDot;
    }

    return {kyHat, krHat, std::sqrt(squaredErrorSum / rmseCount)};
}

Summary simulateIndirect(
    double a,
    double b,
    double am,
    double bm,
    double gammaA,
    double gammaB,
    double observerGain,
    double dt,
    double duration,
    const std::string& csvPath
) {
    std::ofstream csv(csvPath);
    if (!csv) {
        throw std::runtime_error("Cannot open " + csvPath);
    }
    csv << "time,reference,y,ym,u,a_hat,b_hat\n";

    double y = 0.0;
    double ym = 0.0;
    double yHat = 0.0;
    double aHat = 0.6;
    double bHat = 1.4;
    double squaredErrorSum = 0.0;
    std::size_t rmseCount = 0;

    const int steps = static_cast<int>(duration / dt) + 1;
    for (int k = 0; k < steps; ++k) {
        const double t = k * dt;
        const double r = referenceSignal(t);
        const double bSafe = clampValue(bHat, 0.2, 3.0);

        const double kyHat = (aHat - am) / bSafe;
        const double krHat = bm / bSafe;
        const double u = kyHat * y + krHat * r;

        const double predictionError = y - yHat;
        const double trackingError = y - ym;
        const double yDot = -a * y + b * u;
        const double ymDot = -am * ym + bm * r;
        const double yHatDot =
            -aHat * y + bHat * u + observerGain * predictionError;
        const double aHatDot = -gammaA * y * predictionError;
        const double bHatDot = gammaB * u * predictionError;

        csv << std::setprecision(12)
            << t << ',' << r << ',' << y << ',' << ym << ','
            << u << ',' << aHat << ',' << bHat << '\n';

        if (t >= 10.0) {
            squaredErrorSum += trackingError * trackingError;
            ++rmseCount;
        }

        y += dt * yDot;
        ym += dt * ymDot;
        yHat += dt * yHatDot;
        aHat = clampValue(aHat + dt * aHatDot, 0.1, 3.0);
        bHat = clampValue(bHat + dt * bHatDot, 0.2, 3.0);
    }

    return {aHat, bHat, std::sqrt(squaredErrorSum / rmseCount)};
}

int main() {
    try {
        constexpr double aTrue = 1.2;
        constexpr double bTrue = 0.8;
        constexpr double am = 2.0;
        constexpr double bm = 2.0;
        constexpr double dt = 0.001;
        constexpr double duration = 20.0;

        const Summary direct = simulateDirect(
            aTrue, bTrue, am, bm, 2.0, dt, duration,
            "Chapter11_Lesson1_direct.csv"
        );
        const Summary indirect = simulateIndirect(
            aTrue, bTrue, am, bm, 1.5, 1.5, 4.0, dt, duration,
            "Chapter11_Lesson1_indirect.csv"
        );

        std::cout << std::fixed << std::setprecision(6);
        std::cout << "Ideal gains: ky=" << (aTrue - am) / bTrue
                  << ", kr=" << bm / bTrue << '\n';
        std::cout << "Direct final gains: ky_hat=" << direct.p1
                  << ", kr_hat=" << direct.p2 << '\n';
        std::cout << "Indirect final estimates: a_hat=" << indirect.p1
                  << ", b_hat=" << indirect.p2 << '\n';
        std::cout << "Direct RMSE after 10 s: " << direct.rmse_after_10 << '\n';
        std::cout << "Indirect RMSE after 10 s: "
                  << indirect.rmse_after_10 << '\n';
        std::cout << "CSV files were written for plotting.\n";
    } catch (const std::exception& error) {
        std::cerr << "Error: " << error.what() << '\n';
        return 1;
    }
    return 0;
}

11. Java Implementation

The Java implementation uses the standard library and writes the same CSV data as the C++ implementation. Plotting can be added with libraries such as XChart or JFreeChart.

Chapter11_Lesson1.java

// Chapter11_Lesson1.java
// Direct versus indirect adaptive control for a first-order plant.
// Compile: javac Chapter11_Lesson1.java
// Run:     java Chapter11_Lesson1

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

public final class Chapter11_Lesson1 {
    private Chapter11_Lesson1() {}

    private record Summary(double p1, double p2, double rmseAfter10) {}

    private static double referenceSignal(double t) {
        if (t < 3.0) {
            return 1.0;
        }
        if (t < 6.0) {
            return -0.5;
        }
        return 0.8 * Math.sin(0.7 * t) + 0.5 * Math.sin(1.9 * t);
    }

    private static double clamp(double value, double lower, double upper) {
        return Math.min(Math.max(value, lower), upper);
    }

    private static Summary simulateDirect(
            double a,
            double b,
            double am,
            double bm,
            double gamma,
            double dt,
            double duration,
            Path csvPath) throws IOException {

        double y = 0.0;
        double ym = 0.0;
        double kyHat = 0.0;
        double krHat = 0.0;
        double signB = b > 0.0 ? 1.0 : -1.0;
        double squaredErrorSum = 0.0;
        long rmseCount = 0L;
        int steps = (int) (duration / dt) + 1;

        try (BufferedWriter writer = Files.newBufferedWriter(csvPath)) {
            writer.write("time,reference,y,ym,u,ky_hat,kr_hat\n");

            for (int k = 0; k < steps; k++) {
                double t = k * dt;
                double r = referenceSignal(t);
                double u = kyHat * y + krHat * r;
                double e = y - ym;

                double yDot = -a * y + b * u;
                double ymDot = -am * ym + bm * r;
                double kyDot = -gamma * signB * y * e;
                double krDot = -gamma * signB * r * e;

                writer.write(String.format(
                        Locale.US,
                        "%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f%n",
                        t, r, y, ym, u, kyHat, krHat));

                if (t >= 10.0) {
                    squaredErrorSum += e * e;
                    rmseCount++;
                }

                y += dt * yDot;
                ym += dt * ymDot;
                kyHat += dt * kyDot;
                krHat += dt * krDot;
            }
        }

        return new Summary(
                kyHat,
                krHat,
                Math.sqrt(squaredErrorSum / rmseCount));
    }

    private static Summary simulateIndirect(
            double a,
            double b,
            double am,
            double bm,
            double gammaA,
            double gammaB,
            double observerGain,
            double dt,
            double duration,
            Path csvPath) throws IOException {

        double y = 0.0;
        double ym = 0.0;
        double yHat = 0.0;
        double aHat = 0.6;
        double bHat = 1.4;
        double squaredErrorSum = 0.0;
        long rmseCount = 0L;
        int steps = (int) (duration / dt) + 1;

        try (BufferedWriter writer = Files.newBufferedWriter(csvPath)) {
            writer.write("time,reference,y,ym,u,a_hat,b_hat\n");

            for (int k = 0; k < steps; k++) {
                double t = k * dt;
                double r = referenceSignal(t);
                double bSafe = clamp(bHat, 0.2, 3.0);

                double kyHat = (aHat - am) / bSafe;
                double krHat = bm / bSafe;
                double u = kyHat * y + krHat * r;

                double predictionError = y - yHat;
                double trackingError = y - ym;
                double yDot = -a * y + b * u;
                double ymDot = -am * ym + bm * r;
                double yHatDot =
                        -aHat * y
                        + bHat * u
                        + observerGain * predictionError;
                double aHatDot = -gammaA * y * predictionError;
                double bHatDot = gammaB * u * predictionError;

                writer.write(String.format(
                        Locale.US,
                        "%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f%n",
                        t, r, y, ym, u, aHat, bHat));

                if (t >= 10.0) {
                    squaredErrorSum += trackingError * trackingError;
                    rmseCount++;
                }

                y += dt * yDot;
                ym += dt * ymDot;
                yHat += dt * yHatDot;
                aHat = clamp(aHat + dt * aHatDot, 0.1, 3.0);
                bHat = clamp(bHat + dt * bHatDot, 0.2, 3.0);
            }
        }

        return new Summary(
                aHat,
                bHat,
                Math.sqrt(squaredErrorSum / rmseCount));
    }

    public static void main(String[] args) {
        final double aTrue = 1.2;
        final double bTrue = 0.8;
        final double am = 2.0;
        final double bm = 2.0;
        final double dt = 0.001;
        final double duration = 20.0;

        try {
            Summary direct = simulateDirect(
                    aTrue, bTrue, am, bm, 2.0, dt, duration,
                    Path.of("Chapter11_Lesson1_direct.csv"));

            Summary indirect = simulateIndirect(
                    aTrue, bTrue, am, bm, 1.5, 1.5, 4.0, dt, duration,
                    Path.of("Chapter11_Lesson1_indirect.csv"));

            System.out.printf(
                    Locale.US,
                    "Ideal gains: ky=%.6f, kr=%.6f%n",
                    (aTrue - am) / bTrue,
                    bm / bTrue);
            System.out.printf(
                    Locale.US,
                    "Direct final gains: ky_hat=%.6f, kr_hat=%.6f%n",
                    direct.p1(),
                    direct.p2());
            System.out.printf(
                    Locale.US,
                    "Indirect final estimates: a_hat=%.6f, b_hat=%.6f%n",
                    indirect.p1(),
                    indirect.p2());
            System.out.printf(
                    Locale.US,
                    "Direct RMSE after 10 s: %.6f%n",
                    direct.rmseAfter10());
            System.out.printf(
                    Locale.US,
                    "Indirect RMSE after 10 s: %.6f%n",
                    indirect.rmseAfter10());
            System.out.println("CSV files were written for plotting.");
        } catch (IOException error) {
            System.err.println("I/O error: " + error.getMessage());
            System.exit(1);
        }
    }
}

12. MATLAB and Simulink Implementation

The MATLAB script simulates both algorithms, plots their trajectories, and creates a conceptual Simulink architecture model when Simulink is installed. The numerical adaptive laws remain in the MATLAB script so that every equation is visible.

Chapter11_Lesson1.m

%% Chapter11_Lesson1.m
% Direct versus indirect adaptive control for a first-order plant.
% The script simulates both architectures and optionally creates a simple
% Simulink architecture model when Simulink is installed.

clear; clc; close all;

%% Plant and reference-model parameters
aTrue = 1.2;
bTrue = 0.8;
am = 2.0;
bm = 2.0;
dt = 1e-3;
T = 20;
t = (0:dt:T).';
N = numel(t);

r = arrayfun(@referenceSignal, t);

%% Direct adaptive control
yD = zeros(N,1);
ymD = zeros(N,1);
uD = zeros(N,1);
kyHat = zeros(N,1);
krHat = zeros(N,1);
gamma = 2.0;
signB = sign(bTrue);

for k = 1:N-1
    uD(k) = kyHat(k)*yD(k) + krHat(k)*r(k);
    e = yD(k) - ymD(k);

    yDot = -aTrue*yD(k) + bTrue*uD(k);
    ymDot = -am*ymD(k) + bm*r(k);
    kyDot = -gamma*signB*yD(k)*e;
    krDot = -gamma*signB*r(k)*e;

    yD(k+1) = yD(k) + dt*yDot;
    ymD(k+1) = ymD(k) + dt*ymDot;
    kyHat(k+1) = kyHat(k) + dt*kyDot;
    krHat(k+1) = krHat(k) + dt*krDot;
end
uD(N) = kyHat(N)*yD(N) + krHat(N)*r(N);

%% Indirect adaptive control
yI = zeros(N,1);
ymI = zeros(N,1);
yPred = zeros(N,1);
uI = zeros(N,1);
aHat = zeros(N,1);
bHat = zeros(N,1);
aHat(1) = 0.6;
bHat(1) = 1.4;

gammaA = 1.5;
gammaB = 1.5;
observerGain = 4.0;
aBounds = [0.1, 3.0];
bBounds = [0.2, 3.0];

for k = 1:N-1
    bSafe = clampValue(bHat(k), bBounds(1), bBounds(2));
    kyCE = (aHat(k) - am)/bSafe;
    krCE = bm/bSafe;
    uI(k) = kyCE*yI(k) + krCE*r(k);

    predictionError = yI(k) - yPred(k);
    yDot = -aTrue*yI(k) + bTrue*uI(k);
    ymDot = -am*ymI(k) + bm*r(k);
    yPredDot = -aHat(k)*yI(k) + bHat(k)*uI(k) ...
        + observerGain*predictionError;
    aHatDot = -gammaA*yI(k)*predictionError;
    bHatDot = gammaB*uI(k)*predictionError;

    yI(k+1) = yI(k) + dt*yDot;
    ymI(k+1) = ymI(k) + dt*ymDot;
    yPred(k+1) = yPred(k) + dt*yPredDot;
    aHat(k+1) = clampValue( ...
        aHat(k) + dt*aHatDot, aBounds(1), aBounds(2));
    bHat(k+1) = clampValue( ...
        bHat(k) + dt*bHatDot, bBounds(1), bBounds(2));
end
bSafe = clampValue(bHat(N), bBounds(1), bBounds(2));
uI(N) = ((aHat(N)-am)/bSafe)*yI(N) + (bm/bSafe)*r(N);

%% Metrics
idx = t >= 10;
rmseDirect = sqrt(mean((yD(idx)-ymD(idx)).^2));
rmseIndirect = sqrt(mean((yI(idx)-ymI(idx)).^2));

fprintf('Ideal gains: ky = %.6f, kr = %.6f\n', ...
    (aTrue-am)/bTrue, bm/bTrue);
fprintf('Direct final gains: kyHat = %.6f, krHat = %.6f\n', ...
    kyHat(end), krHat(end));
fprintf('Indirect final estimates: aHat = %.6f, bHat = %.6f\n', ...
    aHat(end), bHat(end));
fprintf('Direct RMSE after 10 s: %.6f\n', rmseDirect);
fprintf('Indirect RMSE after 10 s: %.6f\n', rmseIndirect);

%% Plots
figure;
plot(t, ymD, '--', t, yD, t, yI, 'LineWidth', 1.2);
grid on;
xlabel('Time (s)');
ylabel('Output');
title('Direct and indirect adaptive tracking');
legend('Reference model', 'Direct output', 'Indirect output');

figure;
plot(t, kyHat, t, krHat, 'LineWidth', 1.2);
hold on;
yline((aTrue-am)/bTrue, '--');
yline(bm/bTrue, '--');
grid on;
xlabel('Time (s)');
ylabel('Controller parameters');
title('Direct adaptation');
legend('kyHat', 'krHat', 'Ideal ky', 'Ideal kr');

figure;
plot(t, aHat, t, bHat, 'LineWidth', 1.2);
hold on;
yline(aTrue, '--');
yline(bTrue, '--');
grid on;
xlabel('Time (s)');
ylabel('Plant-parameter estimates');
title('Indirect adaptation');
legend('aHat', 'bHat', 'True a', 'True b');

%% Optional Simulink architecture model
if license('test','Simulink')
    buildConceptualSimulinkModel;
else
    fprintf('Simulink is unavailable; numerical simulation is complete.\n');
end

%% Local functions
function r = referenceSignal(t)
    if t < 3
        r = 1.0;
    elseif t < 6
        r = -0.5;
    else
        r = 0.8*sin(0.7*t) + 0.5*sin(1.9*t);
    end
end

function value = clampValue(value, lowerBound, upperBound)
    value = min(max(value, lowerBound), upperBound);
end

function buildConceptualSimulinkModel
    model = 'Chapter11_Lesson1_Architectures';
    if bdIsLoaded(model)
        close_system(model, 0);
    end
    if exist([model '.slx'], 'file')
        delete([model '.slx']);
    end

    new_system(model);
    open_system(model);

    add_block('simulink/Sources/In1', [model '/Reference'], ...
        'Position', [40 80 70 100]);
    add_block('simulink/Ports & Subsystems/Subsystem', ...
        [model '/Direct Adaptive Controller'], ...
        'Position', [140 35 300 110]);
    add_block('simulink/Ports & Subsystems/Subsystem', ...
        [model '/Indirect Identifier plus Controller'], ...
        'Position', [140 150 340 225]);
    add_block('simulink/Ports & Subsystems/Subsystem', ...
        [model '/Plant Copy Direct'], ...
        'Position', [390 35 520 110]);
    add_block('simulink/Ports & Subsystems/Subsystem', ...
        [model '/Plant Copy Indirect'], ...
        'Position', [390 150 520 225]);
    add_block('simulink/Sinks/Out1', [model '/Direct Output'], ...
        'Position', [600 70 630 90]);
    add_block('simulink/Sinks/Out1', [model '/Indirect Output'], ...
        'Position', [600 185 630 205]);

    add_line(model, 'Reference/1', 'Direct Adaptive Controller/1');
    add_line(model, 'Reference/1', ...
        'Indirect Identifier plus Controller/1', 'autorouting', 'on');
    add_line(model, 'Direct Adaptive Controller/1', 'Plant Copy Direct/1');
    add_line(model, 'Indirect Identifier plus Controller/1', ...
        'Plant Copy Indirect/1');
    add_line(model, 'Plant Copy Direct/1', 'Direct Output/1');
    add_line(model, 'Plant Copy Indirect/1', 'Indirect Output/1');

    set_param(model, 'SimulationCommand', 'update');
    save_system(model);
    fprintf('Created conceptual Simulink model: %s.slx\n', model);
end

13. Wolfram Mathematica Implementation

The notebook contains direct and indirect simulations, plots, and error metrics. It uses core Wolfram Language functions and requires no external package.

Chapter11_Lesson1.nb


Notebook[{
  Cell[
    "Chapter 11, Lesson 1: Direct versus Indirect Adaptive Control",
    "Title"
  ],

  Cell[
    "This notebook simulates the same first-order example used in the Python, C++, Java, and MATLAB files.",
    "Text"
  ],

  Cell[
    BoxData[
      "ClearAll[\"Global`*\"];

aTrue = 1.2;
bTrue = 0.8;
am = 2.0;
bm = 2.0;

dt = 0.001;
duration = 20.0;
steps = Round[duration/dt] + 1;

referenceSignal[t_] := Piecewise[
  {
    {1.0, t < 3.0},
    {-0.5, t < 6.0}
  },
  0.8 Sin[0.7 t] + 0.5 Sin[1.9 t]
];

clamp[x_, lo_, hi_] := Min[Max[x, lo], hi];"
    ],
    "Input"
  ],

  Cell[
    BoxData[
      "simulateDirect[] := Module[
  {
    y = 0.,
    ym = 0.,
    ky = 0.,
    kr = 0.,
    gamma = 2.,
    signB = Sign[bTrue],
    rows = {},
    t, r, u, e,
    yDot, ymDot, kyDot, krDot
  },

  Do[
    t = (k - 1) dt;
    r = referenceSignal[t];
    u = ky y + kr r;
    e = y - ym;

    AppendTo[
      rows,
      {t, r, y, ym, u, ky, kr}
    ];

    yDot = -aTrue y + bTrue u;
    ymDot = -am ym + bm r;
    kyDot = -gamma signB y e;
    krDot = -gamma signB r e;

    y += dt yDot;
    ym += dt ymDot;
    ky += dt kyDot;
    kr += dt krDot,
    {k, steps}
  ];

  rows
];

directData = simulateDirect[];"
    ],
    "Input"
  ],

  Cell[
    BoxData[
      "simulateIndirect[] := Module[
  {
    y = 0.,
    ym = 0.,
    yHat = 0.,
    aHat = 0.6,
    bHat = 1.4,
    gammaA = 1.5,
    gammaB = 1.5,
    observerGain = 4.,
    rows = {},
    t, r, bSafe, ky, kr, u, eps,
    yDot, ymDot, yHatDot, aHatDot, bHatDot
  },

  Do[
    t = (k - 1) dt;
    r = referenceSignal[t];

    bSafe = clamp[bHat, 0.2, 3.0];
    ky = (aHat - am)/bSafe;
    kr = bm/bSafe;
    u = ky y + kr r;
    eps = y - yHat;

    AppendTo[
      rows,
      {t, r, y, ym, u, aHat, bHat}
    ];

    yDot = -aTrue y + bTrue u;
    ymDot = -am ym + bm r;

    yHatDot =
      -aHat y +
      bHat u +
      observerGain eps;

    aHatDot = -gammaA y eps;
    bHatDot = gammaB u eps;

    y += dt yDot;
    ym += dt ymDot;
    yHat += dt yHatDot;

    aHat = clamp[
      aHat + dt aHatDot,
      0.1,
      3.0
    ];

    bHat = clamp[
      bHat + dt bHatDot,
      0.2,
      3.0
    ],
    {k, steps}
  ];

  rows
];

indirectData = simulateIndirect[];"
    ],
    "Input"
  ],

  Cell[
    BoxData[
      "directPlot = ListLinePlot[
  {
    directData[[All, {1, 4}]],
    directData[[All, {1, 3}]],
    indirectData[[All, {1, 3}]]
  },
  PlotLegends -> {
    \"Reference model\",
    \"Direct output\",
    \"Indirect output\"
  },
  Frame -> True,
  FrameLabel -> {
    \"Time (s)\",
    \"Output\"
  },
  PlotLabel -> \"Direct and indirect adaptive tracking\",
  ImageSize -> Large
];

directParameterPlot = ListLinePlot[
  {
    directData[[All, {1, 6}]],
    directData[[All, {1, 7}]]
  },
  PlotLegends -> {
    \"kyHat\",
    \"krHat\"
  },
  Frame -> True,
  FrameLabel -> {
    \"Time (s)\",
    \"Controller parameters\"
  },
  ImageSize -> Large
];

indirectParameterPlot = ListLinePlot[
  {
    indirectData[[All, {1, 6}]],
    indirectData[[All, {1, 7}]]
  },
  PlotLegends -> {
    \"aHat\",
    \"bHat\"
  },
  Frame -> True,
  FrameLabel -> {
    \"Time (s)\",
    \"Plant-parameter estimates\"
  },
  ImageSize -> Large
];

Column[
  {
    directPlot,
    directParameterPlot,
    indirectParameterPlot
  }
]"
    ],
    "Input"
  ],

  Cell[
    BoxData[
      "directMask = Select[
  directData,
  First[#] >= 10.0 &
];

indirectMask = Select[
  indirectData,
  First[#] >= 10.0 &
];

rmseDirect = Sqrt[
  Mean[
    (
      directMask[[All, 3]] -
      directMask[[All, 4]]
    )^2
  ]
];

rmseIndirect = Sqrt[
  Mean[
    (
      indirectMask[[All, 3]] -
      indirectMask[[All, 4]]
    )^2
  ]
];

<|
  \"IdealGains\" -> {
    (aTrue - am)/bTrue,
    bm/bTrue
  },

  \"DirectFinalGains\" ->
    directData[[-1, {6, 7}]],

  \"IndirectFinalEstimates\" ->
    indirectData[[-1, {6, 7}]],

  \"DirectRMSEAfter10Seconds\" ->
    rmseDirect,

  \"IndirectRMSEAfter10Seconds\" ->
    rmseIndirect
|>"
    ],
    "Input"
  ]
},
  WindowSize -> {1200, 800},
  StyleDefinitions -> "Default.nb"
]        

14. Interpretation and Practical Pitfalls

14.1 Tracking convergence is not parameter convergence

The direct Lyapunov proof makes the tracking error converge under the stated boundedness assumptions. It does not force the gain estimates to their unique ideal values without persistent excitation. Likewise, a small indirect prediction error does not by itself prove that \( \hat a=a \) and \( \hat b=b \).

14.2 Closed-loop identification is coupled to control

In an indirect architecture, the controller determines the input, the input affects the data available to the identifier, and the identifier changes the controller. Therefore, an identifier that is stable for externally generated bounded data is not automatically a complete closed-loop adaptive-control proof.

14.3 Certainty equivalence needs safeguards

The map \( \hat k_r=b_m/\hat b \) becomes unbounded as \( \hat b \) approaches zero. Projection, known control direction, gain limiting, and actuator saturation handling are therefore not cosmetic implementation details.

14.4 Direct does not mean model-free

Direct adaptive control still depends on structural plant knowledge: relative degree, sign conditions, matching conditions, and a controller parameterization for which ideal gains exist. It is direct with respect to the adjusted parameter vector, not necessarily free of a mathematical plant model.

14.5 Indirect estimates need not be physical

An indirect model may be an input-output parameterization rather than a vector of physical constants. The essential feature is that the estimated model is passed through a controller-design calculation.

15. Problems and Solutions

Problem 1 — Architecture classification: An algorithm updates coefficients \( \hat{\alpha}_1,\hat{\alpha}_2 \) in \( u=\hat{\alpha}_1 y+\hat{\alpha}_2 r \) from tracking error. No plant parameters are estimated. Is the method direct or indirect?

Solution: It is direct. The adjustable vector appears directly in the control law and is a controller-parameter vector. The symbols used for the coefficients do not matter; their role in the architecture determines the classification.


Problem 2 — Direct Lyapunov cancellation: For \( \dot e=-a_m e+b\tilde{\boldsymbol{\theta} }_c^T\boldsymbol{\phi} \), verify that \( \dot{\hat{\boldsymbol{\theta} } }_c =-\gamma\operatorname{sgn}(b)\boldsymbol{\phi}e \) gives \( \dot V=-a_m e^2 \).

Solution: Start from

\[ V = \frac{1}{2}e^2 + \frac{|b|}{2\gamma} \tilde{\boldsymbol{\theta} }_c^T \tilde{\boldsymbol{\theta} }_c . \]

Then

\[ \begin{aligned} \dot V &= -a_m e^2 + b e\tilde{\boldsymbol{\theta} }_c^T\boldsymbol{\phi} + \frac{|b|}{\gamma} \tilde{\boldsymbol{\theta} }_c^T \left( -\gamma\operatorname{sgn}(b)\boldsymbol{\phi}e \right) \\ &= -a_m e^2 + b e\tilde{\boldsymbol{\theta} }_c^T\boldsymbol{\phi} - |b|\operatorname{sgn}(b) e\tilde{\boldsymbol{\theta} }_c^T\boldsymbol{\phi} \\ &=-a_m e^2 . \end{aligned} \]

The cancellation uses \( |b|\operatorname{sgn}(b)=b \).


Problem 3 — Indirect estimator derivation: Starting from \( \dot\varepsilon=-\ell\varepsilon+\tilde a y-\tilde b u \), derive gradient laws that make \( \dot V_I=-\ell\varepsilon^2 \).

Solution: Use

\[ V_I = \frac{1}{2}\varepsilon^2 + \frac{1}{2\gamma_a}\tilde a^2 + \frac{1}{2\gamma_b}\tilde b^2 . \]

Differentiation gives

\[ \dot V_I = -\ell\varepsilon^2 + \tilde a \left( y\varepsilon+\frac{\dot{\hat a} }{\gamma_a} \right) + \tilde b \left( -u\varepsilon+\frac{\dot{\hat b} }{\gamma_b} \right). \]

The two bracketed terms vanish for \( \dot{\hat a}=-\gamma_a y\varepsilon \) and \( \dot{\hat b}=\gamma_b u\varepsilon \).


Problem 4 — Sensitivity of certainty equivalence: Show how an error in \( \hat b \) affects \( \hat k_r=b_m/\hat b \).

Solution: Linearization about the true value gives

\[ \Delta k_r \approx \left. \frac{\partial}{\partial\hat b} \left(\frac{b_m}{\hat b}\right) \right|_{\hat b=b} \Delta b = -\frac{b_m}{b^2}\Delta b . \]

The factor \( b_m/b^2 \) becomes large when \( |b| \) is small. Therefore, a modest estimation error can produce a large controller-gain error near a small input gain.


Problem 5 — Equal behavior, unequal interpretation: Suppose a direct method and an indirect method temporarily generate identical numerical gains \( \hat k_y \) and \( \hat k_r \). Are the architectures then equivalent?

Solution: They generate the same instantaneous control input for the same \( y \) and \( r \), but they are not dynamically equivalent. Their internal states, error signals, update laws, and future gain trajectories differ. The indirect method also contains plant estimates and a synthesis map; the direct method does not require these internal objects.


Problem 6 — Does zero tracking error identify the plant? Assume the direct controller reaches \( e(t)=0 \) for all sufficiently large \( t \). Must \( \hat k_y=k_y^\star \) and \( \hat k_r=k_r^\star \)?

Solution: No. Once the tracking error vanishes, the gradient update also vanishes. If the regressor has not supplied sufficient independent information, several parameter vectors can be consistent with the observed trajectory. Persistent excitation is the additional condition used to rule out such ambiguity.

16. Summary

Direct adaptive control updates controller parameters from a control error without explicitly identifying the plant. Indirect adaptive control estimates plant parameters and converts them into controller parameters through a synthesis map. For the first-order model, a direct Lyapunov law yields a negative-semidefinite tracking-error derivative, while a gradient predictor yields a negative-semidefinite prediction-error derivative for the indirect identifier. The complete indirect loop additionally requires attention to excitation, certainty-equivalence sensitivity, projection, and the feedback coupling between identification and control. The next lesson develops the identifier-plus-controller structure of a self-tuning regulator in greater detail.

17. References

  1. Åström, K.J., & Wittenmark, B. (1973). On self-tuning regulators. Automatica, 9(2), 185–199. https://doi.org/10.1016/0005-1098(73)90073-3
  2. Åström, K.J., Borisson, U., Ljung, L., & Wittenmark, B. (1977). Theory and applications of self-tuning regulators. Automatica, 13(5), 457–476. https://doi.org/10.1016/0005-1098(77)90067-X
  3. Narendra, K.S., & Valavani, L.S. (1978). Stable adaptive controller design—Direct control. IEEE Transactions on Automatic Control, 23(4), 570–583. https://doi.org/10.1109/TAC.1978.1101823
  4. Feuer, A., & Morse, A.S. (1978). Adaptive control of single-input, single-output linear systems. IEEE Transactions on Automatic Control, 23(4), 557–569. https://doi.org/10.1109/TAC.1978.1101822
  5. Morse, A.S. (1980). Global stability of parameter-adaptive control systems. IEEE Transactions on Automatic Control, 25(3), 433–439. https://doi.org/10.1109/TAC.1980.1102364
  6. Narendra, K.S., & Valavani, L.S. (1979). Direct and indirect model reference adaptive control. Automatica, 15(6), 653–664. https://doi.org/10.1016/0005-1098(79)90033-5
  7. Goodwin, G.C., Ramadge, P.J., & Caines, P.E. (1981). Discrete time stochastic adaptive control. SIAM Journal on Control and Optimization, 19(6), 829–853. https://doi.org/10.1137/0319052
  8. Wittenmark, B., & Åström, K.J. (1984). Practical issues in the implementation of self-tuning control. Automatica, 20(5), 595–605. https://doi.org/10.1016/0005-1098(84)90010-4
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.