Chapter 13: Direct Self-Tuning Regulators

Lesson 1: Direct Adaptive Controllers Without Explicit Parameter Estimates

This lesson introduces the defining idea of a direct self-tuning regulator: the adjustable quantities are controller coefficients rather than explicit estimates of the plant coefficients. A first-order model-following controller is derived from first principles, its error equation is obtained, and a Lyapunov argument establishes boundedness and asymptotic tracking under standard matching assumptions. Complete implementations are provided in Python, C++, Java, MATLAB/Simulink, and Wolfram Mathematica.

1. Learning Position and Objectives

Chapters 11 and 12 introduced the indirect self-tuning regulator and recursive estimation. In an indirect architecture, an estimator first produces a plant model and a controller-design map then converts that model into controller coefficients. The present lesson removes that intermediate plant-model estimate.

After completing this lesson, students should be able to:

  • distinguish direct adaptation of controller coefficients from indirect estimation of plant coefficients;
  • derive the ideal controller gains for a matched first-order plant;
  • obtain the tracking-error dynamics in terms of controller-parameter error;
  • construct a Lyapunov update law that does not identify the plant coefficients;
  • explain why tracking convergence does not automatically imply convergence of the adaptive controller gains.

Historically, self-tuning regulators are commonly implemented as sampled-data controllers. To isolate the meaning of direct adaptation before the discrete-time material of Chapter 14, this lesson uses a continuous-time scalar prototype and then implements it numerically. The same architecture later transfers to directly adjusted polynomial controller coefficients.

2. What Makes a Self-Tuning Regulator Direct?

Let \( \boldsymbol{\theta}_p \) denote unknown plant parameters and let \( \boldsymbol{\theta}_c \) denote adjustable controller parameters. An indirect regulator has the conceptual chain

\[ \text{data} \;\rightarrow\; \hat{\boldsymbol{\theta} }_p \;\rightarrow\; \mathcal{D}\!\left(\hat{\boldsymbol{\theta} }_p\right) = \hat{\boldsymbol{\theta} }_c \;\rightarrow\; u , \]

where \( \mathcal{D} \) is a controller-design calculation such as pole placement or minimum-variance synthesis. A direct regulator instead uses

\[ \text{tracking or regulation data} \;\rightarrow\; \hat{\boldsymbol{\theta} }_c \;\rightarrow\; u . \]

The plant parameters may still appear in the mathematical analysis because they determine whether an ideal controller exists. They are not, however, generated as online signals used by the controller.

flowchart TD
  R["Command r"] --> RM["Reference model"]
  R --> C["Controller with adjustable gains"]
  Y["Measured output y"] --> C
  C --> U["Control u"]
  U --> P["Unknown plant"]
  P --> Y
  RM --> E["Tracking error e = y - ym"]
  Y --> E
  E --> A["Direct adaptation law"]
  R --> A
  Y --> A
  A --> C
        

The architecture is called direct because the adaptation block writes directly into the controller. It does not mean that the controller is model-free: the selected controller structure, reference model, matching conditions, control direction, and regressor vector all encode prior structural knowledge.

3. Controller-Space Parameterization

A direct adaptive controller is written as a linear parameterization in controller space:

\[ u(t) = \hat{\boldsymbol{\theta} }_c^{\mathsf T}(t) \boldsymbol{\phi}_c(t), \]

where \( \boldsymbol{\phi}_c(t) \) contains measured or generated controller signals and \( \hat{\boldsymbol{\theta} }_c(t) \) contains the gains actually used in feedback. The corresponding ideal controller vector is defined by an existence statement:

\[ \exists\, \boldsymbol{\theta}_c^\star \quad \text{such that the desired closed-loop error model is obtained.} \]

Define the controller-parameter error by \( \tilde{\boldsymbol{\theta} }_c = \hat{\boldsymbol{\theta} }_c - \boldsymbol{\theta}_c^\star \) . The adaptive law is designed so that the tracking error and \( \tilde{\boldsymbol{\theta} }_c \) form a stable coupled system. Notice the distinction: \( \boldsymbol{\theta}_c^\star \) is used in the proof, but it is never calculated online.

4. First-Order Plant and Reference Model

Consider the uncertain scalar plant

\[ \dot y(t) = a y(t) + b u(t), \qquad b \neq 0 , \]

where \( a \) and the magnitude of \( b \) are not estimated. For this introductory result, the sign of \( b \) is assumed known. Choose the stable reference model

\[ \dot y_m(t) = a_m y_m(t) + b_m r(t), \qquad a_m < 0 , \]

and define \( e(t)=y(t)-y_m(t) \). Use the direct controller

\[ u(t) = \hat k_y(t)y(t) + \hat k_r(t)r(t) = \hat{\boldsymbol{\theta} }^{\mathsf T}(t) \boldsymbol{\phi}(t), \]

\[ \hat{\boldsymbol{\theta} } = \begin{bmatrix} \hat k_y \\ \hat k_r \end{bmatrix}, \qquad \boldsymbol{\phi} = \begin{bmatrix} y \\ r \end{bmatrix} . \]

The adjustable quantities are exactly the feedback and feedforward gains. No variables named \( \hat a \) or \( \hat b \) occur in the controller.

5. Ideal Gains and the Matching Equation

Suppose constant ideal gains \( k_y^\star \) and \( k_r^\star \) exist. Substituting \( u=k_y^\star y+k_r^\star r \) into the plant gives

\[ \dot y = \left(a+b k_y^\star\right)y + b k_r^\star r . \]

Matching this equation to the reference-model coefficients yields

\[ a+b k_y^\star=a_m, \qquad b k_r^\star=b_m , \]

\[ k_y^\star=\frac{a_m-a}{b}, \qquad k_r^\star=\frac{b_m}{b} . \]

These formulas establish existence and identify the matching assumptions. They are not evaluated by the direct controller.

With \( \tilde k_y=\hat k_y-k_y^\star \) and \( \tilde k_r=\hat k_r-k_r^\star \), subtract the model equation from the plant equation:

\[ \begin{aligned} \dot e &= a y+b\hat k_y y+b\hat k_r r -a_m y_m-b_m r \\ &= a_m e + b\tilde k_y y + b\tilde k_r r \\ &= a_m e + b\tilde{\boldsymbol{\theta} }^{\mathsf T} \boldsymbol{\phi} . \end{aligned} \]

This error model is the central simplification: uncertainty enters as a term linear in the controller-parameter error.

6. Direct Adaptive Law and a Lyapunov Proof

Let \( \boldsymbol{\Gamma} =\boldsymbol{\Gamma}^{\mathsf T} \succ 0 \) be an adaptation-gain matrix. Choose

\[ \dot{\hat{\boldsymbol{\theta} } } = -\operatorname{sgn}(b) \boldsymbol{\Gamma} \boldsymbol{\phi} e . \]

For diagonal \( \boldsymbol{\Gamma} =\operatorname{diag}(\gamma_y,\gamma_r) \), this becomes

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

Theorem. Assume \( a_m<0 \), \( b\neq0 \) has known sign, the matching gains exist, and \( r(t) \) and \( \dot r(t) \) are bounded. Then all closed-loop signals are bounded and \( e(t)\rightarrow0 \).

Proof. Consider

\[ V(e,\tilde{\boldsymbol{\theta} }) = \frac{1}{2}e^2 + \frac{|b|}{2} \tilde{\boldsymbol{\theta} }^{\mathsf T} \boldsymbol{\Gamma}^{-1} \tilde{\boldsymbol{\theta} } . \]

Because the ideal gains are constant, \( \dot{\tilde{\boldsymbol{\theta} } } =\dot{\hat{\boldsymbol{\theta} } } \) . Therefore,

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

Thus \( e \) and \( \tilde{\boldsymbol{\theta} } \) are bounded, and

\[ \int_0^\infty e^2(t)\,dt \le \frac{V(0)}{|a_m|} < \infty . \]

Since the stable reference model has bounded output for bounded \( r \), the identity \( y=e+y_m \) implies bounded \( y \). Hence the regressor, adaptive gains, and control input are bounded. The error equation then gives bounded \( \dot e \). Consequently \( e^2 \) is uniformly continuous, and Barbalat's lemma gives \( e(t)\rightarrow0 \). This completes the proof.

The cancellation in \( \dot V \) occurs because the update law changes the controller gains themselves. No plant-estimation error dynamics are introduced.

7. Tracking Convergence Is Not Gain Identification

The proof establishes \( e(t)\rightarrow0 \), but it does not establish \( \hat{\boldsymbol{\theta} }(t) \rightarrow \boldsymbol{\theta}^\star \) . Parameter convergence requires sufficient richness of the controller regressor. A standard persistent-excitation condition is

\[ \exists\,T>0,\;\alpha>0 \quad\text{such that}\quad \int_t^{t+T} \boldsymbol{\phi}(\tau) \boldsymbol{\phi}^{\mathsf T}(\tau) \,d\tau \succeq \alpha\mathbf{I} \quad \forall t\ge0 . \]

Without persistent excitation, several gain vectors may generate the same control action along the observed trajectory. The controller can therefore achieve excellent tracking while its gains settle at values different from the analytical ideal gains. This is not a contradiction: the control objective is trajectory regulation, whereas parameter identification is a stronger objective.

A direct STR should therefore be judged first by closed-loop properties: boundedness, tracking, regulation, control effort, and robustness. Controller-coefficient convergence is secondary unless a later design explicitly requires it.

8. Computational Procedure

flowchart TD
  A["Read command r and measured output y"] --> B["Advance reference model ym"]
  B --> C["Compute error e = y - ym"]
  C --> D["Compute u = gain_y*y + gain_r*r"]
  D --> E["Apply u to the plant"]
  C --> F["Compute gain derivatives from y, r, and e"]
  F --> G["Integrate gain_y and gain_r"]
  G --> D
  E --> A
        

In a continuous implementation, the plant, reference model, and adaptation equations are integrated simultaneously. In digital software, a sufficiently small integration or sampling interval is selected, signals are read, the controller is evaluated, and the gain states are updated. The code examples use fourth-order Runge-Kutta integration so that numerical error does not obscure the adaptive behavior.

9. Numerical Study with an Abrupt Plant Change

The implementations use

\[ a_m=-1.5, \qquad b_m=1.5, \qquad r(t)=1+0.35\sin(0.6t), \]

and the unknown-to-the-controller plant coefficients

\[ (a,b) = \begin{cases} (-0.6,1.2), & 0\le t<20,\\ (-1.0,0.8), & t\ge20. \end{cases} \]

The ideal gains, used only for interpretation, are

Interval \( k_y^\star \) \( k_r^\star \)
\( 0\le t<20 \) \( -0.75 \) \( 1.25 \)
\( t\ge20 \) \( -0.625 \) \( 1.875 \)

The simulation starts with \( \hat k_y(0)=\hat k_r(0)=0 \) and uses \( \gamma_y=\gamma_r=1.5 \). With the supplied program, the RMS tracking error over \( t\ge25\,\text{s} \) is approximately \( 1.81\times10^{-2} \). The final gains need not equal the second pair of ideal gains because the chosen trajectory does not force unique controller-parameter identification.

10. Language Libraries and Implementation Choices

Environment Useful libraries or tools Implementation in this lesson
Python NumPy, SciPy, python-control, Matplotlib NumPy arrays, a from-scratch RK4 integrator, CSV export, and Matplotlib plots.
C++ Eigen, Boost.Odeint, Armadillo, standard library Standard-library-only implementation to expose every adaptive calculation.
Java Apache Commons Math, ojAlgo, EJML From-scratch vector arithmetic and RK4 integration with CSV output.
MATLAB/Simulink Control System Toolbox, Simulink, MATLAB Function blocks, ode45 Transparent fixed-step simulation plus a block-by-block Simulink correspondence.
Wolfram Mathematica NDSolveValue, symbolic algebra, LyapunovSolve, visualization functions Coupled adaptive differential equations solved with NDSolveValue.

Specialized libraries accelerate simulation, but the adaptive law is only a few algebraic operations. Implementing this first example from scratch makes the signal dependencies and stability design visible.

11. Python Implementation

Chapter13_Lesson1.py

# Chapter13_Lesson1.py
# Direct adaptive controller without explicit plant-parameter estimates.

from __future__ import annotations

import csv
import math
from dataclasses import dataclass

import matplotlib.pyplot as plt
import numpy as np


@dataclass(frozen=True)
class Configuration:
    final_time: float = 40.0
    step: float = 0.002
    a_model: float = -1.5
    b_model: float = 1.5
    gamma_y: float = 1.5
    gamma_r: float = 1.5
    change_time: float = 20.0


def reference(t: float) -> float:
    """Bounded command used to reveal tracking and adaptation."""
    return 1.0 + 0.35 * math.sin(0.6 * t)


def plant_parameters(t: float, cfg: Configuration) -> tuple[float, float]:
    """Unknown-to-the-controller plant coefficients used by the simulator."""
    if t < cfg.change_time:
        return -0.6, 1.2
    return -1.0, 0.8


def derivative(t: float, state: np.ndarray, cfg: Configuration) -> np.ndarray:
    y, y_model, gain_y, gain_r = state
    a, b = plant_parameters(t, cfg)
    r = reference(t)
    error = y - y_model

    # The controller adapts its own gains directly; it never computes estimates
    # of a or b. Only the known sign of b is used in the adaptive law.
    control = gain_y * y + gain_r * r
    sign_b = 1.0 if b > 0.0 else -1.0

    return np.array(
        [
            a * y + b * control,
            cfg.a_model * y_model + cfg.b_model * r,
            -cfg.gamma_y * sign_b * y * error,
            -cfg.gamma_r * sign_b * r * error,
        ],
        dtype=float,
    )


def rk4_step(t: float, state: np.ndarray, cfg: Configuration) -> np.ndarray:
    h = cfg.step
    k1 = derivative(t, state, cfg)
    k2 = derivative(t + 0.5 * h, state + 0.5 * h * k1, cfg)
    k3 = derivative(t + 0.5 * h, state + 0.5 * h * k2, cfg)
    k4 = derivative(t + h, state + h * k3, cfg)
    return state + (h / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)


def main() -> None:
    cfg = Configuration()
    time = np.arange(0.0, cfg.final_time + cfg.step, cfg.step)
    state = np.zeros((time.size, 4), dtype=float)

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

    y = state[:, 0]
    y_model = state[:, 1]
    gain_y = state[:, 2]
    gain_r = state[:, 3]
    command = np.array([reference(t) for t in time])
    control = gain_y * y + gain_r * command
    error = y - y_model

    with open("Chapter13_Lesson1_results.csv", "w", newline="", encoding="utf-8") as file:
        writer = csv.writer(file)
        writer.writerow(
            ["time", "reference", "plant_output", "model_output",
             "tracking_error", "control", "gain_y", "gain_r"]
        )
        writer.writerows(
            zip(time, command, y, y_model, error, control, gain_y, gain_r)
        )

    evaluation_mask = time >= 25.0
    rms_error = float(np.sqrt(np.mean(error[evaluation_mask] ** 2)))
    print(f"RMS tracking error for t >= 25 s: {rms_error:.6f}")
    print(f"Final direct gains: gain_y={gain_y[-1]:.6f}, gain_r={gain_r[-1]:.6f}")

    figure, axes = plt.subplots(2, 2, figsize=(11, 7), constrained_layout=True)
    axes[0, 0].plot(time, y, label="plant output")
    axes[0, 0].plot(time, y_model, "--", label="reference model")
    axes[0, 0].set_ylabel("output")
    axes[0, 0].legend()
    axes[0, 0].grid(True)

    axes[0, 1].plot(time, error)
    axes[0, 1].set_ylabel("tracking error")
    axes[0, 1].grid(True)

    axes[1, 0].plot(time, gain_y, label="gain_y")
    axes[1, 0].plot(time, gain_r, label="gain_r")
    axes[1, 0].set_xlabel("time [s]")
    axes[1, 0].set_ylabel("controller gains")
    axes[1, 0].legend()
    axes[1, 0].grid(True)

    axes[1, 1].plot(time, control)
    axes[1, 1].set_xlabel("time [s]")
    axes[1, 1].set_ylabel("control input")
    axes[1, 1].grid(True)

    figure.suptitle("Direct adaptive control without explicit estimates of a or b")
    plt.show()


if __name__ == "__main__":
    main()

12. C++ Implementation

Chapter13_Lesson1.cpp

// Chapter13_Lesson1.cpp
// Direct adaptive controller implemented from scratch with the C++ standard library.

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

struct Configuration {
    double finalTime = 40.0;
    double step = 0.002;
    double aModel = -1.5;
    double bModel = 1.5;
    double gammaY = 1.5;
    double gammaR = 1.5;
    double changeTime = 20.0;
};

using State = std::array<double, 4>;

double referenceSignal(double t) {
    return 1.0 + 0.35 * std::sin(0.6 * t);
}

std::array<double, 2> plantParameters(double t, const Configuration& cfg) {
    if (t < cfg.changeTime) {
        return {-0.6, 1.2};
    }
    return {-1.0, 0.8};
}

State addScaled(const State& x, const State& direction, double scale) {
    State result{};
    for (std::size_t i = 0; i < x.size(); ++i) {
        result[i] = x[i] + scale * direction[i];
    }
    return result;
}

State derivative(double t, const State& x, const Configuration& cfg) {
    const double y = x[0];
    const double yModel = x[1];
    const double gainY = x[2];
    const double gainR = x[3];

    const auto [a, b] = plantParameters(t, cfg);
    const double r = referenceSignal(t);
    const double error = y - yModel;
    const double control = gainY * y + gainR * r;
    const double signB = (b > 0.0) ? 1.0 : -1.0;

    return {
        a * y + b * control,
        cfg.aModel * yModel + cfg.bModel * r,
        -cfg.gammaY * signB * y * error,
        -cfg.gammaR * signB * r * error
    };
}

State rk4Step(double t, const State& x, const Configuration& cfg) {
    const double h = cfg.step;
    const State k1 = derivative(t, x, cfg);
    const State k2 = derivative(t + 0.5 * h, addScaled(x, k1, 0.5 * h), cfg);
    const State k3 = derivative(t + 0.5 * h, addScaled(x, k2, 0.5 * h), cfg);
    const State k4 = derivative(t + h, addScaled(x, k3, h), cfg);

    State next{};
    for (std::size_t i = 0; i < x.size(); ++i) {
        next[i] = x[i] + (h / 6.0) * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]);
    }
    return next;
}

int main() {
    const Configuration cfg;
    const int sampleCount = static_cast<int>(std::llround(cfg.finalTime / cfg.step)) + 1;
    State state{0.0, 0.0, 0.0, 0.0};

    std::ofstream output("Chapter13_Lesson1_results_cpp.csv");
    if (!output) {
        throw std::runtime_error("Could not create the CSV output file.");
    }
    output << "time,reference,plant_output,model_output,tracking_error,control,gain_y,gain_r\n";
    output << std::setprecision(12);

    double squaredErrorSum = 0.0;
    int evaluationSamples = 0;

    for (int k = 0; k < sampleCount; ++k) {
        const double t = k * cfg.step;
        const double r = referenceSignal(t);
        const double y = state[0];
        const double yModel = state[1];
        const double error = y - yModel;
        const double control = state[2] * y + state[3] * r;

        output << t << ',' << r << ',' << y << ',' << yModel << ','
               << error << ',' << control << ',' << state[2] << ',' << state[3] << '\n';

        if (t >= 25.0) {
            squaredErrorSum += error * error;
            ++evaluationSamples;
        }
        if (k + 1 < sampleCount) {
            state = rk4Step(t, state, cfg);
        }
    }

    const double rmsError = std::sqrt(squaredErrorSum / evaluationSamples);
    std::cout << "RMS tracking error for t >= 25 s: " << rmsError << '\n';
    std::cout << "Final direct gains: gain_y=" << state[2]
              << ", gain_r=" << state[3] << '\n';
    return 0;
}

Compile with g++ -std=c++17 -O2 Chapter13_Lesson1.cpp -o Chapter13_Lesson1.

13. Java Implementation

Chapter13_Lesson1.java

// Chapter13_Lesson1.java
// Direct adaptive controller implemented from scratch in Java.

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Locale;

public final class Chapter13_Lesson1 {
    private static final double FINAL_TIME = 40.0;
    private static final double STEP = 0.002;
    private static final double A_MODEL = -1.5;
    private static final double B_MODEL = 1.5;
    private static final double GAMMA_Y = 1.5;
    private static final double GAMMA_R = 1.5;
    private static final double CHANGE_TIME = 20.0;

    private Chapter13_Lesson1() {
    }

    private static double referenceSignal(double t) {
        return 1.0 + 0.35 * Math.sin(0.6 * t);
    }

    private static double[] plantParameters(double t) {
        if (t < CHANGE_TIME) {
            return new double[] {-0.6, 1.2};
        }
        return new double[] {-1.0, 0.8};
    }

    private static double[] derivative(double t, double[] state) {
        double y = state[0];
        double yModel = state[1];
        double gainY = state[2];
        double gainR = state[3];

        double[] plant = plantParameters(t);
        double a = plant[0];
        double b = plant[1];
        double r = referenceSignal(t);
        double error = y - yModel;
        double control = gainY * y + gainR * r;
        double signB = b > 0.0 ? 1.0 : -1.0;

        return new double[] {
            a * y + b * control,
            A_MODEL * yModel + B_MODEL * r,
            -GAMMA_Y * signB * y * error,
            -GAMMA_R * signB * r * error
        };
    }

    private static double[] addScaled(double[] state, double[] direction, double scale) {
        double[] result = new double[state.length];
        for (int i = 0; i < state.length; i++) {
            result[i] = state[i] + scale * direction[i];
        }
        return result;
    }

    private static double[] rk4Step(double t, double[] state) {
        double[] k1 = derivative(t, state);
        double[] k2 = derivative(t + 0.5 * STEP, addScaled(state, k1, 0.5 * STEP));
        double[] k3 = derivative(t + 0.5 * STEP, addScaled(state, k2, 0.5 * STEP));
        double[] k4 = derivative(t + STEP, addScaled(state, k3, STEP));

        double[] next = new double[state.length];
        for (int i = 0; i < state.length; i++) {
            next[i] = state[i] + (STEP / 6.0)
                * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]);
        }
        return next;
    }

    public static void main(String[] args) throws IOException {
        Locale.setDefault(Locale.US);
        int sampleCount = (int) Math.round(FINAL_TIME / STEP) + 1;
        double[] state = {0.0, 0.0, 0.0, 0.0};
        double squaredErrorSum = 0.0;
        int evaluationSamples = 0;

        try (BufferedWriter writer =
                 new BufferedWriter(new FileWriter("Chapter13_Lesson1_results_java.csv"))) {
            writer.write(
                "time,reference,plant_output,model_output,tracking_error,"
                + "control,gain_y,gain_r\n"
            );

            for (int k = 0; k < sampleCount; k++) {
                double t = k * STEP;
                double r = referenceSignal(t);
                double y = state[0];
                double yModel = state[1];
                double error = y - yModel;
                double control = state[2] * y + state[3] * r;

                writer.write(String.format(
                    Locale.US,
                    "%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f%n",
                    t, r, y, yModel, error, control, state[2], state[3]
                ));

                if (t >= 25.0) {
                    squaredErrorSum += error * error;
                    evaluationSamples++;
                }
                if (k + 1 < sampleCount) {
                    state = rk4Step(t, state);
                }
            }
        }

        double rmsError = Math.sqrt(squaredErrorSum / evaluationSamples);
        System.out.printf(Locale.US, "RMS tracking error for t >= 25 s: %.6f%n", rmsError);
        System.out.printf(
            Locale.US,
            "Final direct gains: gain_y=%.6f, gain_r=%.6f%n",
            state[2], state[3]
        );
    }
}

Compile with javac Chapter13_Lesson1.java and run with java Chapter13_Lesson1.

14. MATLAB and Simulink Implementation

Chapter13_Lesson1.m

% Chapter13_Lesson1.m
% Direct adaptive controller without explicit plant-parameter estimates.
% The fixed-step RK4 loop is transparent and maps directly to a Simulink
% realization using Integrator, Sum, Product, Gain, and MATLAB Function blocks.

clear; clc; close all;

finalTime = 40.0;
step = 0.002;
aModel = -1.5;
bModel = 1.5;
gammaY = 1.5;
gammaR = 1.5;
changeTime = 20.0;

time = (0:step:finalTime).';
state = zeros(numel(time), 4); % [y, yModel, gainY, gainR]

for k = 1:(numel(time) - 1)
    t = time(k);
    x = state(k, :).';

    k1 = dynamics(t, x, aModel, bModel, gammaY, gammaR, changeTime);
    k2 = dynamics(t + step/2, x + step*k1/2, ...
        aModel, bModel, gammaY, gammaR, changeTime);
    k3 = dynamics(t + step/2, x + step*k2/2, ...
        aModel, bModel, gammaY, gammaR, changeTime);
    k4 = dynamics(t + step, x + step*k3, ...
        aModel, bModel, gammaY, gammaR, changeTime);

    state(k + 1, :) = (x + step*(k1 + 2*k2 + 2*k3 + k4)/6).';
end

reference = 1.0 + 0.35*sin(0.6*time);
plantOutput = state(:, 1);
modelOutput = state(:, 2);
gainY = state(:, 3);
gainR = state(:, 4);
trackingError = plantOutput - modelOutput;
control = gainY.*plantOutput + gainR.*reference;

results = table(time, reference, plantOutput, modelOutput, trackingError, ...
    control, gainY, gainR);
writetable(results, 'Chapter13_Lesson1_results_matlab.csv');

mask = time >= 25.0;
rmsError = sqrt(mean(trackingError(mask).^2));
fprintf('RMS tracking error for t >= 25 s: %.6f\n', rmsError);
fprintf('Final direct gains: gainY=%.6f, gainR=%.6f\n', gainY(end), gainR(end));

figure('Name', 'Chapter 13 Lesson 1: Direct Adaptive Controller');
tiledlayout(2, 2);

nexttile;
plot(time, plantOutput, time, modelOutput, '--', 'LineWidth', 1.1);
grid on; xlabel('Time [s]'); ylabel('Output');
legend('Plant output', 'Reference model', 'Location', 'best');

nexttile;
plot(time, trackingError, 'LineWidth', 1.1);
grid on; xlabel('Time [s]'); ylabel('Tracking error');

nexttile;
plot(time, gainY, time, gainR, 'LineWidth', 1.1);
grid on; xlabel('Time [s]'); ylabel('Controller gains');
legend('gainY', 'gainR', 'Location', 'best');

nexttile;
plot(time, control, 'LineWidth', 1.1);
grid on; xlabel('Time [s]'); ylabel('Control input');

% Simulink correspondence:
% 1. Use two Integrator blocks for y and yModel.
% 2. Form e = y - yModel with a Sum block.
% 3. Form u = gainY*y + gainR*r with Product and Sum blocks.
% 4. Integrate gainY_dot = -gammaY*sign(b)*y*e.
% 5. Integrate gainR_dot = -gammaR*sign(b)*r*e.
% 6. Keep the plant coefficients inside the plant subsystem; do not route
%    estimates of a or b into the adaptive controller.

function dx = dynamics(t, x, aModel, bModel, gammaY, gammaR, changeTime)
    y = x(1);
    yModel = x(2);
    gainY = x(3);
    gainR = x(4);

    r = 1.0 + 0.35*sin(0.6*t);
    if t < changeTime
        a = -0.6;
        b = 1.2;
    else
        a = -1.0;
        b = 0.8;
    end

    error = y - yModel;
    control = gainY*y + gainR*r;
    signB = sign(b);

    dx = [
        a*y + b*control;
        aModel*yModel + bModel*r;
        -gammaY*signB*y*error;
        -gammaR*signB*r*error
    ];
end

In Simulink, place the plant and reference model in separate subsystems. Use two additional Integrator blocks for \( \hat k_y \) and \( \hat k_r \). The controller subsystem receives only \( y \), \( r \), and the two adaptive gains; the plant coefficients remain internal to the simulated plant and are not exported as estimates.

15. Wolfram Mathematica Implementation

Chapter13_Lesson1.nb

Notebook[{
  Cell["Chapter 13, Lesson 1: Direct Adaptive Controllers Without Explicit Parameter Estimates", "Title"],
  Cell["Evaluate the following Wolfram Language input cell.", "Text"],
  Cell[BoxData[ToBoxes[Defer[
ClearAll["Global`*"];

aModel = -1.5;
bModel = 1.5;
gammaY = 1.5;
gammaR = 1.5;
changeTime = 20.0;
finalTime = 40.0;

reference[t_] := 1.0 + 0.35 Sin[0.6 t];
aPlant[t_] := Piecewise[{ {-0.6, t < changeTime} }, -1.0];
bPlant[t_] := Piecewise[{ {1.2, t < changeTime} }, 0.8];

solution = NDSolveValue[
  {
    y'[t] == aPlant[t] y[t] +
      bPlant[t] (gainY[t] y[t] + gainR[t] reference[t]),
    yModel'[t] == aModel yModel[t] + bModel reference[t],
    gainY'[t] == -gammaY Sign[bPlant[t]] y[t] (y[t] - yModel[t]),
    gainR'[t] == -gammaR Sign[bPlant[t]] reference[t] (y[t] - yModel[t]),
    y[0] == 0.0,
    yModel[0] == 0.0,
    gainY[0] == 0.0,
    gainR[0] == 0.0
  },
  {y, yModel, gainY, gainR},
  {t, 0.0, finalTime},
  MaxStepFraction -> 1/5000
];

timeGrid = Subdivide[0.0, finalTime, 4000];
plantOutput = solution[[1]] /@ timeGrid;
modelOutput = solution[[2]] /@ timeGrid;
gainYData = solution[[3]] /@ timeGrid;
gainRData = solution[[4]] /@ timeGrid;
referenceData = reference /@ timeGrid;
trackingError = plantOutput - modelOutput;
controlData = gainYData plantOutput + gainRData referenceData;

Export[
  "Chapter13_Lesson1_results_mathematica.csv",
  Prepend[
    Transpose[
      {timeGrid, referenceData, plantOutput, modelOutput, trackingError,
       controlData, gainYData, gainRData}
    ],
    {"time", "reference", "plant_output", "model_output", "tracking_error",
     "control", "gain_y", "gain_r"}
  ]
];

rmsError = Sqrt[
  Mean[
    Pick[trackingError^2, UnitStep[timeGrid - 25.0], 1]
  ]
];
Print["RMS tracking error for t >= 25 s: ", N[rmsError, 8]];
Print[
  "Final direct gains: gainY=", N[Last[gainYData], 8],
  ", gainR=", N[Last[gainRData], 8]
];

GraphicsGrid[
  {
    {
      ListLinePlot[
        {Transpose[{timeGrid, plantOutput}],
         Transpose[{timeGrid, modelOutput}]},
        PlotLegends -> {"Plant output", "Reference model"},
        Frame -> True,
        FrameLabel -> {"Time [s]", "Output"},
        PlotRange -> All
      ],
      ListLinePlot[
        Transpose[{timeGrid, trackingError}],
        Frame -> True,
        FrameLabel -> {"Time [s]", "Tracking error"},
        PlotRange -> All
      ]
    },
    {
      ListLinePlot[
        {Transpose[{timeGrid, gainYData}],
         Transpose[{timeGrid, gainRData}]},
        PlotLegends -> {"gainY", "gainR"},
        Frame -> True,
        FrameLabel -> {"Time [s]", "Controller gains"},
        PlotRange -> All
      ],
      ListLinePlot[
        Transpose[{timeGrid, controlData}],
        Frame -> True,
        FrameLabel -> {"Time [s]", "Control input"},
        PlotRange -> All
      ]
    }
  },
  ImageSize -> Large
]
  ]]], "Input"]
}]

16. Interpretation, Assumptions, and Limitations

Known control direction. The update law uses \( \operatorname{sgn}(b) \). An unknown sign requires a different mechanism and is treated in Chapter 16.

Matching. The reference-model coefficients must be reachable by the selected controller structure. If no constant ideal gains exist, the cancellation used in the proof is unavailable.

Exact model class. The theorem assumes the first-order plant description is exact. Noise, delays, unmodeled dynamics, and actuator limits can cause gain drift or high-frequency control activity. Projection, normalization, leakage, dead zones, and robust modifications introduced in earlier chapters are practical extensions.

Time-varying plant coefficients. The formal proof above uses constant ideal gains. The abrupt-change simulation is an engineering stress test: after the change, a new constant ideal controller exists, and the direct gains readjust. A rigorous theorem for continuously time-varying ideal gains would include additional terms involving \( \dot{\boldsymbol{\theta} }^\star(t) \) and generally yields bounded tracking rather than exact asymptotic convergence.

Direct does not mean computation-free. Direct adaptation avoids the repeated map from plant estimates to controller coefficients, but the adaptation law still requires filtering, numerical integration, signal validation, and safeguards in a real implementation.

17. Problems and Solutions

Problem 1: Derivation of the ideal direct gains. Consider \( \dot y=-0.4y+2u \) and the reference model \( \dot y_m=-1.2y_m+1.8r \). For \( u=k_y y+k_r r \), find the ideal gains.

Solution. Matching the coefficients gives

\[ -0.4+2k_y^\star=-1.2, \qquad 2k_r^\star=1.8 . \]

\[ k_y^\star = \frac{-1.2+0.4}{2} = -0.4, \qquad k_r^\star = \frac{1.8}{2} = 0.9 . \]

A direct adaptive controller does not calculate these values online. They certify that the selected feedback structure can reproduce the desired model.

Problem 2: Complete the Lyapunov cancellation. For the scalar adaptive law \( \dot{\hat k}_y=-\gamma_y \operatorname{sgn}(b)ye \) and \( \dot{\hat k}_r=-\gamma_r \operatorname{sgn}(b)re \), verify directly that \( \dot V=a_m e^2 \) for

\[ V = \frac{1}{2}e^2 + \frac{|b|}{2\gamma_y}\tilde k_y^2 + \frac{|b|}{2\gamma_r}\tilde k_r^2 . \]

Solution. Differentiate and insert the error model:

\[ \begin{aligned} \dot V &= e\left(a_m e+b\tilde k_y y+b\tilde k_r r\right) + \frac{|b|}{\gamma_y} \tilde k_y\dot{\hat k}_y + \frac{|b|}{\gamma_r} \tilde k_r\dot{\hat k}_r \\ &= a_m e^2 + b e\tilde k_y y + b e\tilde k_r r - |b|\operatorname{sgn}(b)e\tilde k_y y - |b|\operatorname{sgn}(b)e\tilde k_r r \\ &= a_m e^2 , \end{aligned} \]

because \( |b|\operatorname{sgn}(b)=b \). The controller-parameter cross terms cancel exactly.

Problem 3: Why a constant command may not identify both gains. Suppose tracking is perfect at a constant operating point with \( r(t)=r_0 \) and \( y(t)=y_0 \). Show why one steady-state equation cannot uniquely determine both direct gains.

Solution. At equilibrium,

\[ 0 = a y_0 + b\left(\hat k_y y_0+\hat k_r r_0\right). \]

This is one linear constraint on the two unknown controller gains:

\[ y_0\hat k_y+r_0\hat k_r = -\frac{a}{b}y_0 . \]

Unless independent transient or frequency content supplies another direction in the regressor space, infinitely many gain pairs satisfy this equation. Tracking may be exact while the parameter vector remains nonunique.

Problem 4: Ideal gains before and after a plant change. For the numerical example in Section 9, derive the two ideal gain vectors.

Solution. Before the change,

\[ k_y^\star = \frac{-1.5-(-0.6)}{1.2} = -0.75, \qquad k_r^\star = \frac{1.5}{1.2} = 1.25 . \]

After the change,

\[ k_y^\star = \frac{-1.5-(-1.0)}{0.8} = -0.625, \qquad k_r^\star = \frac{1.5}{0.8} = 1.875 . \]

The adaptive controller is not informed of either pair. The coefficient change appears only through the measured tracking error, which restarts adaptation.

Problem 5: Direct versus indirect signal inventory. Write the internal adjustable signals required by an indirect STR for the plant \( \dot y=ay+bu \), and compare them with the direct controller of this lesson.

Solution. A basic indirect design maintains

\[ \hat a(t), \qquad \hat b(t), \qquad \hat k_y(t) = \frac{a_m-\hat a(t)}{\hat b(t)}, \qquad \hat k_r(t) = \frac{b_m}{\hat b(t)} . \]

It therefore contains a plant estimator and an algebraic controller redesign. The direct controller maintains only \( \hat k_y(t) \) and \( \hat k_r(t) \). It avoids division by \( \hat b(t) \) and does not expose plant estimates, but it still assumes a valid controller parameterization and a known control direction.

18. Summary

A direct self-tuning regulator adapts controller coefficients without first estimating a plant model. For the first-order matched plant, the controller \( u=\hat k_y y+\hat k_r r \) and the update law \( \dot{\hat{\boldsymbol{\theta} } } =-\operatorname{sgn}(b) \boldsymbol{\Gamma}\boldsymbol{\phi}e \) yield a Lyapunov derivative \( \dot V=a_m e^2\le0 \). Under bounded-reference assumptions, all closed-loop signals remain bounded and the tracking error converges to zero. The ideal gains appear only in analysis; tracking can converge even when the adaptive gains do not converge to a unique ideal vector. Lesson 2 will generalize the parameterization of controller gains and reference-model objectives.

19. References

  1. Åström, K.J., & Wittenmark, B. (1973). On self-tuning regulators. Automatica, 9(2), 185–199.
  2. Åström, K.J., Borisson, U., Ljung, L., & Wittenmark, B. (1977). Theory and applications of self-tuning regulators. Automatica, 13(5), 457–476.
  3. Clarke, D.W., & Gawthrop, P.J. (1975). Self-tuning controller. Proceedings of the Institution of Electrical Engineers, 122(9), 929–934.
  4. Gawthrop, P.J. (1980). On the stability and convergence of a self-tuning controller. International Journal of Control, 31(5), 973–998.
  5. Goodwin, G.C., Ramadge, P.J., & Caines, P.E. (1980). Discrete-time multivariable adaptive control. IEEE Transactions on Automatic Control, 25(3), 449–456.
  6. Morse, A.S. (1980). Global stability of parameter-adaptive control systems. IEEE Transactions on Automatic Control, 25(3), 433–439.
  7. Landau, I.D., & Lozano, R. (1981). Unification of discrete-time explicit model-reference adaptive control designs. Automatica, 17(4), 593–611.
  8. Goodwin, G.C., Hill, D.J., & Palaniswami, M. (1984). A perspective on convergence of adaptive control algorithms. Automatica, 20(5), 519–531.
  9. Radenković, M.S., & Stanković, S.S. (1990). Strong consistency of parameter estimates in direct self-tuning control algorithms based on stochastic approximation. Automatica, 26(3), 533–544.
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.