Chapter 20: Gain Scheduling and Linear Parameter-Varying (LPV) Ideas

Lesson 2: Gain Scheduling vs True Adaptive Control

This lesson establishes a precise mathematical boundary between classical gain scheduling and true adaptive control. Both vary controller coefficients during operation, but only adaptive control closes a feedback loop around controller performance and gives the controller parameters their own dynamics. We derive comparable error systems, prove a basic MRAC result, analyze scheduling mismatch and parameter-rate effects, and implement fixed, scheduled, adaptive, and hybrid controllers on one common plant.

1. Learning Objectives and the Central Distinction

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

  • define gain scheduling as a precomputed map from measurable operating conditions to controller coefficients;
  • define true adaptation as an online dynamic update driven by tracking or prediction information;
  • derive scalar tracking-error models for scheduled and adaptive controllers;
  • prove stability of a basic direct adaptive law under a constant matched uncertainty;
  • explain why stable frozen operating-point controllers do not automatically imply stability of the time-varying interconnection;
  • select fixed, scheduled, adaptive, or scheduled-plus-adaptive architectures for a stated uncertainty structure.

The superficial similarity is that both approaches produce time-varying gains. The structural difference is the source of that variation. A classical schedule evaluates a designer-supplied map such as \( K=K(\rho) \). An adaptive controller integrates an update law such as \( \dot{\hat\theta}=\mathcal{A}(e,\phi,\hat\theta) \). Therefore, a gain can change with time without being adaptive.

flowchart TD
  R["Command r"] --> P1["Plant"]
  P1 --> Y["Measured output y"]
  Y --> E["Tracking error e"]
  S["Measured operating condition rho"] --> MAP["Offline schedule K(rho)"]
  MAP --> C1["Scheduled controller"]
  C1 --> P1
  E --> LAW["Online update law"]
  LAW --> PAR["Adaptive parameter state theta_hat"]
  PAR --> C2["Adaptive controller"]
  C2 --> P1
        

In the upper path, the performance error is not used to modify the schedule. In the lower path, the error explicitly drives a parameter state. A practical hybrid may contain both paths, but they remain conceptually separable.

2. Common Plant and Reference-Model Setup

Consider the scalar plant

\[ \dot{x}(t)=a(\rho(t),t)x(t)+b u(t), \qquad b>0, \]

where \( \rho(t) \) is a measurable operating-condition variable. It may be speed, altitude, payload, temperature, or another signal correlated with plant dynamics. The desired response is specified by the stable reference model

\[ \dot{x}_m(t)=-\lambda x_m(t)+\lambda r(t), \qquad \lambda>0. \]

Use a controller parameterization common to all four architectures:

\[ u(t)=\theta(t)x(t)+k_r r(t), \qquad k_r=\frac{\lambda}{b}. \]

Define the tracking error \( e=x-x_m \). Subtracting the reference model from the plant gives

\[ \dot{e}=\left[a(\rho,t)+b\theta(t)\right]x-\lambda r+\lambda x_m =-\lambda e+\left[a(\rho,t)+b\theta(t)+\lambda\right]x. \]

The instantaneous ideal gain is therefore

\[ \theta^{\ast}(\rho,t)=\frac{-\lambda-a(\rho,t)}{b}. \]

This common equation exposes the real question: how is \( \theta(t) \) generated, and what information is available to generate it?

3. Classical Gain Scheduling

Suppose offline modeling provides a nominal coefficient map \( a_s(\rho) \). The scheduled feedback gain is chosen as

\[ \theta_s(\rho)=\frac{-\lambda-a_s(\rho)}{b}. \]

Substitution into the error dynamics yields

\[ \dot{e}=-\lambda e+\Delta_a(\rho,t)x, \qquad \Delta_a(\rho,t)=a(\rho,t)-a_s(\rho). \]

When the schedule is exact for this scalar static-feedback example, \( \Delta_a=0 \) and the desired model is obtained immediately. No online learning is required. When the map is biased, stale, or missing a latent variable, the mismatch acts as a state-dependent disturbance.

3.1 Grid construction and interpolation

Let controllers be designed at grid points \( \rho_1,\ldots,\rho_N \). For \( \rho\in[\rho_i,\rho_{i+1}] \), linear interpolation gives

\[ \theta_s(\rho)=\alpha_i(\rho)\theta_i+ \left[1-\alpha_i(\rho)\right]\theta_{i+1}, \qquad \alpha_i(\rho)=\frac{\rho_{i+1}-\rho}{\rho_{i+1}-\rho_i}. \]

Smooth interpolation prevents command discontinuities, but arbitrary gain interpolation need not preserve closed-loop stability. Stability-preserving interpolation is a design property, not a consequence of smooth numerical blending.

3.2 Frozen-time reasoning and its limitation

A frozen-time test fixes \( \rho=\bar\rho \) and checks whether \( A_{cl}(\bar\rho) \) is Hurwitz. This is necessary for many schedules but is not generally sufficient for the time-varying system

\[ \dot{x}=A_{cl}(\rho(t))x. \]

The trajectory depends on products of transition matrices generated at different parameter values. A family of individually stable matrices can create difficult transients when eigenvectors, controller states, or scheduling rates vary. A common quadratic certificate avoids this particular problem: if there exists \( P=P^T>0 \) and \( Q=Q^T>0 \) such that

\[ A_{cl}(\rho)^T P+P A_{cl}(\rho)\le -Q \quad \text{for all admissible }\rho, \]

then \( V=x^TPx \) proves uniform exponential stability:

\[ \dot{V}\le -\lambda_{\min}(Q)\|x\|^2 \le -\frac{\lambda_{\min}(Q)}{\lambda_{\max}(P)}V. \]

4. True Adaptive Control

For the basic direct adaptive case, assume that \( a \) is an unknown constant during the proof interval and that \( b>0 \) is known. Implement

\[ u=\hat\theta x+\frac{\lambda}{b}r, \qquad \theta^{\ast}=\frac{-\lambda-a}{b}, \qquad \widetilde\theta=\hat\theta-\theta^{\ast}. \]

The error system becomes

\[ \dot{e}=-\lambda e+b\widetilde\theta x. \]

Unlike a schedule, \( \hat\theta \) is a dynamic state. Choose the update law

\[ \dot{\hat\theta}=-\gamma x e, \qquad \gamma>0. \]

The update uses closed-loop performance information. It does not require a precomputed mapping from \( \rho \) to the correct gain.

4.1 Lyapunov proof

Consider the positive-definite function

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

Because the ideal parameter is constant,

\[ \dot{\widetilde\theta}=\dot{\hat\theta}. \]

Its derivative is

\[ \begin{aligned} \dot V &=e\left(-\lambda e+b\widetilde\theta x\right) +\frac{b}{\gamma}\widetilde\theta\dot{\hat\theta} \\ &=-\lambda e^2+b\widetilde\theta xe +\frac{b}{\gamma}\widetilde\theta\left(-\gamma xe\right) \\ &=-\lambda e^2\le 0. \end{aligned} \]

Hence \( e \) and \( \widetilde\theta \) are bounded, \( e\in L_2 \), and—under the usual bounded-signal conditions needed to bound \( \dot e \)—Barbalat's lemma gives \( e(t)\rightarrow 0 \). Parameter convergence is a different statement and generally requires persistent excitation, as established in Chapter 10.

4.2 Time-varying ideal parameter

If \( a=a(\rho(t),t) \), then \( \theta^{\ast}=\theta^{\ast}(t) \) and

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

The same calculation becomes

\[ \dot V=-\lambda e^2- \frac{b}{\gamma}\widetilde\theta\dot{\theta}^{\ast}. \]

The last term is not sign-definite. Thus a basic constant-parameter MRAC proof must not be silently reused for arbitrarily fast time-varying parameters. Projection, leakage, normalization, rate bounds, or a scheduled nominal term can be used to obtain practical boundedness results.

5. Side-by-Side Mathematical Comparison

For the common plant, the three variable-gain cases can be written compactly:

\[ \begin{array}{lll} \text{Gain scheduling:} & \theta=\theta_s(\rho), & \dot e=-\lambda e+\Delta_a x, \\ \text{Adaptive control:} & \dot{\hat\theta}=-\gamma xe, & \dot e=-\lambda e+b\widetilde\theta x, \\ \text{Hybrid:} & \theta=\theta_s(\rho)+\hat\delta,\; \dot{\hat\delta}=-\gamma xe, & \dot e=-\lambda e+\left(\Delta_a+b\hat\delta\right)x. \end{array} \]

Scheduling has no parameter-error dynamics unless the designer adds them. Adaptation has no guaranteed knowledge of the operating envelope unless the designer supplies it. The hybrid assigns known repeatable variation to the schedule and reserves adaptation for residual uncertainty.

Property Fixed controller Gain scheduling True adaptive control
Controller parameters Constant Static map of measured operating condition Dynamic states updated online
Primary information source One nominal model Offline model family and scheduling variable Closed-loop tracking or prediction error
Learning after deployment None None in classical scheduling Yes, within the chosen parameterization
Response to known envelope variation Usually weak Often fast and predictable Possible, but adaptation has transient dynamics
Response to unmodeled parameter bias Persistent error Persistent scheduling mismatch Can compensate when matching and excitation are adequate
Stability argument Single closed-loop model Must cover interpolation and parameter trajectories Coupled state-parameter Lyapunov analysis
Certification burden Lowest Grid, interpolation, rate, and transition validation Update law, bounds, excitation, robustness, and transient validation

6. What Can Break a Gain Schedule?

6.1 A poor scheduling variable

A scheduling variable should explain the dominant plant variation and be available with acceptable noise and delay. If two different plant states share the same \( \rho \) but require different gains, the map \( K(\rho) \) is structurally ambiguous.

6.2 Scheduling-variable dynamics and delay

With a delayed measurement \( \rho_m(t)=\rho(t-\tau) \), the implemented mismatch is

\[ \Delta_a(t)=a(\rho(t),t)-a_s(\rho(t-\tau)). \]

If \( a_s \) is Lipschitz with constant \( L_a \) and \( |\dot\rho|\le \nu \), then

\[ |a_s(\rho(t))-a_s(\rho(t-\tau))|\le L_a\nu\tau. \]

This converts sensor delay and scheduling rate directly into a model-error bound.

6.3 Controller-state realization

For a dynamic controller \( \dot x_c=A_c(\rho)x_c+B_c(\rho)e \), simply changing matrices may create bumps because the stored controller state was generated under a previous realization. Equivalent frozen transfer functions can have different state coordinates. A consistent parameter-dependent realization or explicit state transformation is therefore part of gain-schedule design.

6.4 Interpolation outside the design grid

Interpolation is an in-envelope operation; extrapolation is a new controller design. A safe implementation clamps the scheduling variable, enters a fallback controller, or proves a larger admissible set. Silent extrapolation should be treated as an unverified operating mode.

7. What Can Break an Adaptive Controller?

Adaptation can compensate only uncertainties represented by the controller parameterization and matching conditions. It is not an unrestricted online optimizer.

  • Insufficient excitation: tracking may converge while parameters do not.
  • Unmodeled dynamics: aggressive adaptation can excite neglected fast modes.
  • Noise: the product \( xe \) can inject high-frequency parameter motion.
  • Saturation: the adaptation law may interpret actuator clipping as parameter error.
  • Fast parameter variation: the ideal parameter derivative appears as a disturbance in the Lyapunov derivative.
  • Incorrect control direction: the simple law above assumes the known sign \( b>0 \).

Chapters 8, 9, 10, 16, and 19 supplied the appropriate robust modifications, projection, excitation concepts, unknown-control-direction tools, and saturation handling. The present lesson uses projection in software to prevent numerical drift, but projection does not repair an invalid plant parameterization.

8. Scheduled Nominal Control with Adaptive Augmentation

Decompose the implemented gain as

\[ \theta(t)=\theta_s(\rho(t))+\hat\delta(t). \]

The schedule handles repeatable envelope variation; the adaptive increment handles residual mismatch. With \( \Delta_a=a-a_s \), the error equation is

\[ \dot e=-\lambda e+\left[\Delta_a(\rho,t)+b\hat\delta\right]x. \]

If the residual can be represented by an ideal correction \( \delta^{\ast}=-\Delta_a/b \), define \( \widetilde\delta=\hat\delta-\delta^{\ast} \) and update

\[ \dot{\hat\delta}=-\gamma xe. \]

For constant residual mismatch, the previous Lyapunov proof applies with \( \widetilde\theta \) replaced by \( \widetilde\delta \). The initial adaptive burden is often smaller because the nominal schedule already places the loop near its desired behavior.

flowchart TD
  R["Command r"] --> SUM["Control computation"]
  X["Measured state x"] --> SUM
  RHO["Operating condition rho"] --> SCH["Nominal schedule theta_s(rho)"]
  SCH --> SUM
  X --> ERR["Tracking error e = x - x_m"]
  XM["Reference model x_m"] --> ERR
  ERR --> ADP["Residual update delta_hat_dot = -gamma*x*e"]
  ADP --> SUM
  SUM --> U["u = (theta_s + delta_hat)*x + k_r*r"]
  U --> PLANT["Plant"]
  PLANT --> X
        

9. Reproducible Numerical Experiment

The implementations use

\[ a_s(\rho)=0.4+0.75\rho, \qquad a(\rho,t)=0.4+1.00\rho+0.18\sin(0.37t), \]

\[ \rho(t)=0.5+0.45\sin(0.18t), \qquad b=1.2, \quad \lambda=1.5, \quad \gamma=2.8. \]

The nominal schedule intentionally omits part of the parameter dependence and a time-varying residual. Four controllers are compared: one fixed gain, the nominal schedule, pure adaptation initialized at the fixed gain, and scheduled control with an adaptive residual. The code reports tracking RMSE and peak control magnitude and exports all trajectories to CSV.

9.1 Python

Dependencies: numpy and matplotlib.

Chapter20_Lesson2.py

# Chapter20_Lesson2.py
# Gain scheduling versus true adaptive control for a scalar LPV-like plant.

from __future__ import annotations

import csv
from dataclasses import dataclass
from typing import Callable

import matplotlib.pyplot as plt
import numpy as np


@dataclass
class SimulationResult:
    x: np.ndarray
    xm: np.ndarray
    u: np.ndarray
    theta: np.ndarray


def reference(t: float) -> float:
    if t < 2.0:
        return 0.0
    if t < 16.0:
        return 1.0
    if t < 28.0:
        return -0.6
    return 0.8


def scheduling_variable(t: float) -> float:
    return 0.5 + 0.45 * np.sin(0.18 * t)


def a_nominal(rho: float) -> float:
    return 0.4 + 0.75 * rho


def a_true(t: float, rho: float) -> float:
    # The last term is not represented in the precomputed schedule.
    return 0.4 + 1.00 * rho + 0.18 * np.sin(0.37 * t)


def project(value: float, lower: float, upper: float) -> float:
    return float(np.clip(value, lower, upper))


def simulate(mode: str, t: np.ndarray, dt: float) -> SimulationResult:
    b = 1.2
    lam = 1.5
    kr = lam / b
    gamma = 2.8

    x = np.zeros_like(t)
    xm = np.zeros_like(t)
    u = np.zeros_like(t)
    theta = np.zeros_like(t)

    theta_fixed = (-lam - a_nominal(0.5)) / b
    theta[0] = theta_fixed
    delta = 0.0

    for k in range(t.size - 1):
        tk = float(t[k])
        rho = scheduling_variable(tk)
        r = reference(tk)
        e = x[k] - xm[k]
        theta_schedule = (-lam - a_nominal(rho)) / b

        if mode == "fixed":
            theta_k = theta_fixed
            theta_next = theta_k
        elif mode == "scheduled":
            theta_k = theta_schedule
            theta_next = (-lam - a_nominal(scheduling_variable(t[k + 1]))) / b
        elif mode == "adaptive":
            theta_k = theta[k]
            theta_dot = -gamma * x[k] * e
            theta_next = project(theta_k + dt * theta_dot, -6.0, 1.0)
        elif mode == "hybrid":
            delta_dot = -gamma * x[k] * e
            delta = project(delta + dt * delta_dot, -2.0, 2.0)
            theta_k = theta_schedule + delta
            theta_next = (-lam - a_nominal(scheduling_variable(t[k + 1]))) / b + delta
        else:
            raise ValueError(f"Unknown mode: {mode}")

        u[k] = theta_k * x[k] + kr * r
        x_dot = a_true(tk, rho) * x[k] + b * u[k]
        xm_dot = -lam * xm[k] + lam * r

        x[k + 1] = x[k] + dt * x_dot
        xm[k + 1] = xm[k] + dt * xm_dot
        theta[k + 1] = theta_next

    u[-1] = u[-2]
    return SimulationResult(x=x, xm=xm, u=u, theta=theta)


def rmse(signal: np.ndarray, target: np.ndarray, start_index: int) -> float:
    error = signal[start_index:] - target[start_index:]
    return float(np.sqrt(np.mean(error * error)))


def main() -> None:
    dt = 0.001
    final_time = 40.0
    t = np.arange(0.0, final_time + dt, dt)
    modes = ("fixed", "scheduled", "adaptive", "hybrid")
    results = {mode: simulate(mode, t, dt) for mode in modes}

    start_index = int(2.0 / dt)
    for mode in modes:
        result = results[mode]
        print(
            f"{mode:10s} RMSE={rmse(result.x, result.xm, start_index):.6f} "
            f"max|u|={np.max(np.abs(result.u)):.6f}"
        )

    csv_path = "Chapter20_Lesson2_results.csv"
    with open(csv_path, "w", newline="", encoding="utf-8") as csv_file:
        writer = csv.writer(csv_file)
        writer.writerow(
            ["time", "rho", "reference", "xm"]
            + [f"x_{mode}" for mode in modes]
            + [f"u_{mode}" for mode in modes]
            + [f"theta_{mode}" for mode in modes]
        )
        for k, tk in enumerate(t):
            writer.writerow(
                [tk, scheduling_variable(float(tk)), reference(float(tk)), results["fixed"].xm[k]]
                + [results[mode].x[k] for mode in modes]
                + [results[mode].u[k] for mode in modes]
                + [results[mode].theta[k] for mode in modes]
            )

    plt.figure(figsize=(10, 6))
    plt.plot(t, results["fixed"].xm, "k--", label="reference model")
    for mode in modes:
        plt.plot(t, results[mode].x, label=mode)
    plt.xlabel("time [s]")
    plt.ylabel("state")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()

    plt.figure(figsize=(10, 5))
    for mode in modes:
        plt.plot(t, results[mode].theta, label=mode)
    plt.xlabel("time [s]")
    plt.ylabel("implemented feedback gain")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()

    plt.figure(figsize=(10, 4))
    plt.plot(t, [scheduling_variable(float(tk)) for tk in t])
    plt.xlabel("time [s]")
    plt.ylabel("scheduling variable rho")
    plt.grid(True)
    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    main()

9.2 C++

The C++17 implementation uses only the standard library.

Chapter20_Lesson2.cpp

// Chapter20_Lesson2.cpp
// Gain scheduling versus true adaptive control for a scalar LPV-like plant.

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

struct Result {
    std::vector<double> x;
    std::vector<double> xm;
    std::vector<double> u;
    std::vector<double> theta;
};

double reference(double t) {
    if (t < 2.0) return 0.0;
    if (t < 16.0) return 1.0;
    if (t < 28.0) return -0.6;
    return 0.8;
}

double schedulingVariable(double t) {
    return 0.5 + 0.45 * std::sin(0.18 * t);
}

double aNominal(double rho) {
    return 0.4 + 0.75 * rho;
}

double aTrue(double t, double rho) {
    return 0.4 + 1.00 * rho + 0.18 * std::sin(0.37 * t);
}

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

Result simulate(const std::string& mode, const std::vector<double>& time, double dt) {
    const double b = 1.2;
    const double lambda = 1.5;
    const double kr = lambda / b;
    const double gamma = 2.8;
    const std::size_t n = time.size();

    Result result{std::vector<double>(n, 0.0), std::vector<double>(n, 0.0),
                  std::vector<double>(n, 0.0), std::vector<double>(n, 0.0)};

    const double thetaFixed = (-lambda - aNominal(0.5)) / b;
    result.theta[0] = thetaFixed;
    double delta = 0.0;

    for (std::size_t k = 0; k + 1 < n; ++k) {
        const double t = time[k];
        const double rho = schedulingVariable(t);
        const double r = reference(t);
        const double e = result.x[k] - result.xm[k];
        const double thetaSchedule = (-lambda - aNominal(rho)) / b;
        double thetaK = thetaFixed;
        double thetaNext = thetaFixed;

        if (mode == "fixed") {
            thetaK = thetaFixed;
            thetaNext = thetaK;
        } else if (mode == "scheduled") {
            thetaK = thetaSchedule;
            thetaNext = (-lambda - aNominal(schedulingVariable(time[k + 1]))) / b;
        } else if (mode == "adaptive") {
            thetaK = result.theta[k];
            const double thetaDot = -gamma * result.x[k] * e;
            thetaNext = project(thetaK + dt * thetaDot, -6.0, 1.0);
        } else if (mode == "hybrid") {
            const double deltaDot = -gamma * result.x[k] * e;
            delta = project(delta + dt * deltaDot, -2.0, 2.0);
            thetaK = thetaSchedule + delta;
            thetaNext = (-lambda - aNominal(schedulingVariable(time[k + 1]))) / b + delta;
        } else {
            throw std::invalid_argument("Unknown mode: " + mode);
        }

        result.u[k] = thetaK * result.x[k] + kr * r;
        const double xDot = aTrue(t, rho) * result.x[k] + b * result.u[k];
        const double xmDot = -lambda * result.xm[k] + lambda * r;

        result.x[k + 1] = result.x[k] + dt * xDot;
        result.xm[k + 1] = result.xm[k] + dt * xmDot;
        result.theta[k + 1] = thetaNext;
    }

    result.u[n - 1] = result.u[n - 2];
    return result;
}

double rmse(const std::vector<double>& signal, const std::vector<double>& target,
            std::size_t startIndex) {
    double sum = 0.0;
    for (std::size_t k = startIndex; k < signal.size(); ++k) {
        const double e = signal[k] - target[k];
        sum += e * e;
    }
    return std::sqrt(sum / static_cast<double>(signal.size() - startIndex));
}

double maxAbs(const std::vector<double>& values) {
    double maximum = 0.0;
    for (double value : values) maximum = std::max(maximum, std::abs(value));
    return maximum;
}

int main() {
    const double dt = 0.001;
    const double finalTime = 40.0;
    const std::size_t n = static_cast<std::size_t>(finalTime / dt) + 1;
    std::vector<double> time(n, 0.0);
    for (std::size_t k = 0; k < n; ++k) time[k] = static_cast<double>(k) * dt;

    const std::vector<std::string> modes{"fixed", "scheduled", "adaptive", "hybrid"};
    std::vector<Result> results;
    for (const auto& mode : modes) results.push_back(simulate(mode, time, dt));

    const std::size_t startIndex = static_cast<std::size_t>(2.0 / dt);
    for (std::size_t i = 0; i < modes.size(); ++i) {
        std::cout << std::setw(10) << modes[i] << " RMSE=" << std::fixed
                  << std::setprecision(6) << rmse(results[i].x, results[i].xm, startIndex)
                  << " max|u|=" << maxAbs(results[i].u) << '\n';
    }

    std::ofstream file("Chapter20_Lesson2_results_cpp.csv");
    if (!file) {
        std::cerr << "Cannot create output CSV file.\n";
        return 1;
    }
    file << "time,rho,reference,xm";
    for (const auto& mode : modes) file << ",x_" << mode;
    for (const auto& mode : modes) file << ",u_" << mode;
    for (const auto& mode : modes) file << ",theta_" << mode;
    file << '\n';

    file << std::setprecision(12);
    for (std::size_t k = 0; k < n; ++k) {
        file << time[k] << ',' << schedulingVariable(time[k]) << ',' << reference(time[k])
             << ',' << results[0].xm[k];
        for (const auto& result : results) file << ',' << result.x[k];
        for (const auto& result : results) file << ',' << result.u[k];
        for (const auto& result : results) file << ',' << result.theta[k];
        file << '\n';
    }
    return 0;
}

9.3 Java

The Java implementation uses only the standard library and writes a CSV file.

Chapter20_Lesson2.java

// Chapter20_Lesson2.java
// Gain scheduling versus true adaptive control for a scalar LPV-like plant.

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

public final class Chapter20_Lesson2 {
    private static final double B = 1.2;
    private static final double LAMBDA = 1.5;
    private static final double KR = LAMBDA / B;
    private static final double GAMMA = 2.8;

    private static final class Result {
        final double[] x;
        final double[] xm;
        final double[] u;
        final double[] theta;

        Result(int n) {
            x = new double[n];
            xm = new double[n];
            u = new double[n];
            theta = new double[n];
        }
    }

    private static double reference(double t) {
        if (t < 2.0) return 0.0;
        if (t < 16.0) return 1.0;
        if (t < 28.0) return -0.6;
        return 0.8;
    }

    private static double schedulingVariable(double t) {
        return 0.5 + 0.45 * Math.sin(0.18 * t);
    }

    private static double aNominal(double rho) {
        return 0.4 + 0.75 * rho;
    }

    private static double aTrue(double t, double rho) {
        return 0.4 + 1.00 * rho + 0.18 * Math.sin(0.37 * t);
    }

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

    private static Result simulate(String mode, double[] time, double dt) {
        Result result = new Result(time.length);
        double thetaFixed = (-LAMBDA - aNominal(0.5)) / B;
        result.theta[0] = thetaFixed;
        double delta = 0.0;

        for (int k = 0; k < time.length - 1; k++) {
            double t = time[k];
            double rho = schedulingVariable(t);
            double r = reference(t);
            double e = result.x[k] - result.xm[k];
            double thetaSchedule = (-LAMBDA - aNominal(rho)) / B;
            double thetaK;
            double thetaNext;

            switch (mode) {
                case "fixed":
                    thetaK = thetaFixed;
                    thetaNext = thetaK;
                    break;
                case "scheduled":
                    thetaK = thetaSchedule;
                    thetaNext = (-LAMBDA - aNominal(schedulingVariable(time[k + 1]))) / B;
                    break;
                case "adaptive":
                    thetaK = result.theta[k];
                    double thetaDot = -GAMMA * result.x[k] * e;
                    thetaNext = project(thetaK + dt * thetaDot, -6.0, 1.0);
                    break;
                case "hybrid":
                    double deltaDot = -GAMMA * result.x[k] * e;
                    delta = project(delta + dt * deltaDot, -2.0, 2.0);
                    thetaK = thetaSchedule + delta;
                    thetaNext = (-LAMBDA - aNominal(schedulingVariable(time[k + 1]))) / B + delta;
                    break;
                default:
                    throw new IllegalArgumentException("Unknown mode: " + mode);
            }

            result.u[k] = thetaK * result.x[k] + KR * r;
            double xDot = aTrue(t, rho) * result.x[k] + B * result.u[k];
            double xmDot = -LAMBDA * result.xm[k] + LAMBDA * r;

            result.x[k + 1] = result.x[k] + dt * xDot;
            result.xm[k + 1] = result.xm[k] + dt * xmDot;
            result.theta[k + 1] = thetaNext;
        }
        result.u[time.length - 1] = result.u[time.length - 2];
        return result;
    }

    private static double rmse(double[] signal, double[] target, int startIndex) {
        double sum = 0.0;
        for (int k = startIndex; k < signal.length; k++) {
            double e = signal[k] - target[k];
            sum += e * e;
        }
        return Math.sqrt(sum / (signal.length - startIndex));
    }

    private static double maxAbs(double[] values) {
        double maximum = 0.0;
        for (double value : values) maximum = Math.max(maximum, Math.abs(value));
        return maximum;
    }

    public static void main(String[] args) throws IOException {
        Locale.setDefault(Locale.US);
        double dt = 0.001;
        double finalTime = 40.0;
        int n = (int) Math.round(finalTime / dt) + 1;
        double[] time = new double[n];
        for (int k = 0; k < n; k++) time[k] = k * dt;

        String[] modes = {"fixed", "scheduled", "adaptive", "hybrid"};
        Map<String, Result> results = new LinkedHashMap<>();
        for (String mode : modes) results.put(mode, simulate(mode, time, dt));

        int startIndex = (int) Math.round(2.0 / dt);
        for (String mode : modes) {
            Result result = results.get(mode);
            System.out.printf(
                "%10s RMSE=%.6f max|u|=%.6f%n",
                mode, rmse(result.x, result.xm, startIndex), maxAbs(result.u)
            );
        }

        try (PrintWriter writer = new PrintWriter(
                new BufferedWriter(new FileWriter("Chapter20_Lesson2_results_java.csv")))) {
            writer.print("time,rho,reference,xm");
            for (String mode : modes) writer.print(",x_" + mode);
            for (String mode : modes) writer.print(",u_" + mode);
            for (String mode : modes) writer.print(",theta_" + mode);
            writer.println();

            for (int k = 0; k < n; k++) {
                writer.printf("%.12f,%.12f,%.12f,%.12f", time[k], schedulingVariable(time[k]),
                    reference(time[k]), results.get("fixed").xm[k]);
                for (String mode : modes) writer.printf(",%.12f", results.get(mode).x[k]);
                for (String mode : modes) writer.printf(",%.12f", results.get(mode).u[k]);
                for (String mode : modes) writer.printf(",%.12f", results.get(mode).theta[k]);
                writer.println();
            }
        }
    }
}

9.4 MATLAB and Simulink

The MATLAB script performs the same numerical experiment. Its final comments specify the corresponding Simulink integrators, lookup-table/function block, parameter update, and projection realization.

Chapter20_Lesson2.m

% Chapter20_Lesson2.m
% Gain scheduling versus true adaptive control for a scalar LPV-like plant.

clear; clc; close all;

dt = 0.001;
T = 40;
t = (0:dt:T)';
modes = {'fixed', 'scheduled', 'adaptive', 'hybrid'};
results = struct();

for i = 1:numel(modes)
    results.(modes{i}) = simulateMode(modes{i}, t, dt);
end

startIndex = round(2 / dt) + 1;
for i = 1:numel(modes)
    mode = modes{i};
    e = results.(mode).x(startIndex:end) - results.(mode).xm(startIndex:end);
    rmseValue = sqrt(mean(e.^2));
    maxU = max(abs(results.(mode).u));
    fprintf('%10s RMSE=%.6f max|u|=%.6f\n', mode, rmseValue, maxU);
end

rho = arrayfun(@schedulingVariable, t);
r = arrayfun(@referenceSignal, t);
output = table(t, rho, r, results.fixed.xm, ...
    results.fixed.x, results.scheduled.x, results.adaptive.x, results.hybrid.x, ...
    results.fixed.u, results.scheduled.u, results.adaptive.u, results.hybrid.u, ...
    results.fixed.theta, results.scheduled.theta, results.adaptive.theta, results.hybrid.theta, ...
    'VariableNames', {'time','rho','reference','xm', ...
    'x_fixed','x_scheduled','x_adaptive','x_hybrid', ...
    'u_fixed','u_scheduled','u_adaptive','u_hybrid', ...
    'theta_fixed','theta_scheduled','theta_adaptive','theta_hybrid'});
writetable(output, 'Chapter20_Lesson2_results_matlab.csv');

figure;
plot(t, results.fixed.xm, 'k--', 'LineWidth', 1.5); hold on;
plot(t, results.fixed.x, t, results.scheduled.x, t, results.adaptive.x, t, results.hybrid.x);
grid on; xlabel('time [s]'); ylabel('state');
legend('reference model','fixed','scheduled','adaptive','hybrid', 'Location','best');

figure;
plot(t, results.fixed.theta, t, results.scheduled.theta, ...
    t, results.adaptive.theta, t, results.hybrid.theta);
grid on; xlabel('time [s]'); ylabel('implemented feedback gain');
legend('fixed','scheduled','adaptive','hybrid', 'Location','best');

figure;
plot(t, rho); grid on;
xlabel('time [s]'); ylabel('scheduling variable rho');

% Simulink realization:
% 1. Use Integrator blocks for x, xm, and (for adaptive modes) theta or delta.
% 2. Use a MATLAB Function or 1-D Lookup Table block for a_nominal(rho).
% 3. Form u = theta*x + kr*r using Product and Sum blocks.
% 4. For adaptation, implement theta_dot = -gamma*x*(x-xm).
% 5. Place a Saturation block after the parameter integrator to realize projection.

function result = simulateMode(mode, t, dt)
    b = 1.2;
    lambda = 1.5;
    kr = lambda / b;
    gamma = 2.8;
    n = numel(t);

    x = zeros(n, 1);
    xm = zeros(n, 1);
    u = zeros(n, 1);
    theta = zeros(n, 1);

    thetaFixed = (-lambda - aNominal(0.5)) / b;
    theta(1) = thetaFixed;
    delta = 0;

    for k = 1:n-1
        tk = t(k);
        rho = schedulingVariable(tk);
        r = referenceSignal(tk);
        e = x(k) - xm(k);
        thetaSchedule = (-lambda - aNominal(rho)) / b;

        switch mode
            case 'fixed'
                thetaK = thetaFixed;
                thetaNext = thetaK;
            case 'scheduled'
                thetaK = thetaSchedule;
                thetaNext = (-lambda - aNominal(schedulingVariable(t(k+1)))) / b;
            case 'adaptive'
                thetaK = theta(k);
                thetaDot = -gamma * x(k) * e;
                thetaNext = project(thetaK + dt * thetaDot, -6, 1);
            case 'hybrid'
                deltaDot = -gamma * x(k) * e;
                delta = project(delta + dt * deltaDot, -2, 2);
                thetaK = thetaSchedule + delta;
                thetaNext = (-lambda - aNominal(schedulingVariable(t(k+1)))) / b + delta;
            otherwise
                error('Unknown mode: %s', mode);
        end

        u(k) = thetaK * x(k) + kr * r;
        xDot = aTrue(tk, rho) * x(k) + b * u(k);
        xmDot = -lambda * xm(k) + lambda * r;

        x(k+1) = x(k) + dt * xDot;
        xm(k+1) = xm(k) + dt * xmDot;
        theta(k+1) = thetaNext;
    end
    u(end) = u(end-1);
    result = struct('x', x, 'xm', xm, 'u', u, 'theta', theta);
end

function value = referenceSignal(t)
    if t < 2
        value = 0;
    elseif t < 16
        value = 1;
    elseif t < 28
        value = -0.6;
    else
        value = 0.8;
    end
end

function rho = schedulingVariable(t)
    rho = 0.5 + 0.45 * sin(0.18 * t);
end

function value = aNominal(rho)
    value = 0.4 + 0.75 * rho;
end

function value = aTrue(t, rho)
    value = 0.4 + 1.00 * rho + 0.18 * sin(0.37 * t);
end

function value = project(value, lower, upper)
    value = min(max(value, lower), upper);
end

9.5 Wolfram Mathematica

The notebook contains a Wolfram Language input cell implementing the same Euler simulation, metrics, plots, and PNG exports.

Chapter20_Lesson2.nb


Notebook[{
 Cell["Chapter 20, Lesson 2: Gain Scheduling vs True Adaptive Control", "Title"],
 Cell[BoxData["ClearAll[reference, schedulingVariable, aNominal, aTrue, project, simulate, rmse];
dt = 0.001; finalTime = 40.; time = N@Range[0., finalTime, dt];
b = 1.2; lambda = 1.5; kr = lambda/b; gamma = 2.8;
reference[t_] := Piecewise[{ {0., t < 2.}, {1., t < 16.}, {-0.6, t < 28.} }, 0.8];
schedulingVariable[t_] := 0.5 + 0.45 Sin[0.18 t];
aNominal[rho_] := 0.4 + 0.75 rho;
aTrue[t_, rho_] := 0.4 + 1.00 rho + 0.18 Sin[0.37 t];
project[value_, lower_, upper_] := Clip[value, {lower, upper}];
simulate[mode_String] := Module[
 {n = Length[time], x, xm, u, theta, thetaFixed, delta = 0., k, t, rho,
  r, e, thetaSchedule, thetaK, thetaNext, thetaDot, deltaDot, xDot, xmDot},
 x = ConstantArray[0., n]; xm = ConstantArray[0., n];
 u = ConstantArray[0., n]; theta = ConstantArray[0., n];
 thetaFixed = (-lambda - aNominal[0.5])/b; theta[[1]] = thetaFixed;
 For[k = 1, k < n, k++,
  t = time[[k]]; rho = schedulingVariable[t]; r = reference[t];
  e = x[[k]] - xm[[k]]; thetaSchedule = (-lambda - aNominal[rho])/b;
  Switch[mode,
   \"fixed\", thetaK = thetaFixed; thetaNext = thetaK,
   \"scheduled\", thetaK = thetaSchedule;
    thetaNext = (-lambda - aNominal[schedulingVariable[time[[k + 1]]]])/b,
   \"adaptive\", thetaK = theta[[k]]; thetaDot = -gamma x[[k]] e;
    thetaNext = project[thetaK + dt thetaDot, -6., 1.],
   \"hybrid\", deltaDot = -gamma x[[k]] e;
    delta = project[delta + dt deltaDot, -2., 2.];
    thetaK = thetaSchedule + delta;
    thetaNext = (-lambda - aNominal[schedulingVariable[time[[k + 1]]]])/b + delta,
   _, Print[\"Unknown mode: \", mode]; Abort[]
  ];
  u[[k]] = thetaK x[[k]] + kr r;
  xDot = aTrue[t, rho] x[[k]] + b u[[k]];
  xmDot = -lambda xm[[k]] + lambda r;
  x[[k + 1]] = x[[k]] + dt xDot; xm[[k + 1]] = xm[[k]] + dt xmDot;
  theta[[k + 1]] = thetaNext;
 ];
 u[[-1]] = u[[-2]];
 <|\"x\" -> x, \"xm\" -> xm, \"u\" -> u, \"theta\" -> theta|>
];
modes = {\"fixed\", \"scheduled\", \"adaptive\", \"hybrid\"};
results = AssociationMap[simulate, modes]; startIndex = Round[2./dt] + 1;
rmse[signal_, target_] := Sqrt[Mean[(signal[[startIndex ;;]] - target[[startIndex ;;]])^2]];
Do[Print[StringForm[\"`` RMSE=`` max|u|=``\", mode,
 NumberForm[rmse[results[mode][\"x\"], results[mode][\"xm\"]], {8, 6}],
 NumberForm[Max[Abs[results[mode][\"u\"]]], {8, 6}]]], {mode, modes}];
trackingPlot = ListLinePlot[
 Join[{ {time, results[\"fixed\"][\"xm\"]} }, Table[{time, results[mode][\"x\"]}, {mode, modes}]],
 PlotLegends -> Join[{\"reference model\"}, modes],
 AxesLabel -> {\"time [s]\", \"state\"}, PlotRange -> All, ImageSize -> Large];
gainPlot = ListLinePlot[Table[{time, results[mode][\"theta\"]}, {mode, modes}],
 PlotLegends -> modes, AxesLabel -> {\"time [s]\", \"implemented feedback gain\"},
 PlotRange -> All, ImageSize -> Large];
rhoPlot = ListLinePlot[Transpose[{time, schedulingVariable /@ time}],
 AxesLabel -> {\"time [s]\", \"scheduling variable rho\"}, ImageSize -> Large];
Export[\"Chapter20_Lesson2_tracking.png\", trackingPlot]; Export[\"Chapter20_Lesson2_gains.png\", gainPlot];
Export[\"Chapter20_Lesson2_rho.png\", rhoPlot];
Column[{trackingPlot, gainPlot, rhoPlot}]
"], "Input"]
}, WindowSize -> {1200, 800}, StyleDefinitions -> "Default.nb"]        

10. Interpreting the Experiment

The fixed controller is designed at one operating point and therefore accumulates mismatch as \( \rho \) moves. The scheduled controller removes the portion predicted by \( a_s(\rho) \) immediately, without waiting for an error transient. Pure adaptation must first observe error and then move its parameter state. The hybrid starts from the scheduled value and adapts only the residual.

A lower RMSE in this example does not establish universal superiority. The ranking changes with schedule quality, adaptation gain, reference excitation, measurement noise, saturation, and parameter rate. The correct comparison is architectural:

\[ \text{known repeatable variation}\;\Rightarrow\;\text{schedule}, \qquad \text{unknown matched residual}\;\Rightarrow\;\text{adaptation}. \]

The two mechanisms are complementary when their responsibilities are explicitly separated and independently bounded.

11. Design Decision Rules

  1. Use a fixed controller when one design meets performance and robustness requirements over the complete certified envelope.
  2. Use gain scheduling when dominant variation is repeatable, measurable, and accurately mapped offline, especially when immediate predictable transients are important.
  3. Use adaptive control when an important matched uncertainty is not known before operation and sufficient closed-loop information exists to update it safely.
  4. Use scheduled-plus-adaptive control when a trustworthy nominal envelope model exists but residual bias, aging, damage, payload variation, or manufacturing dispersion remains.
  5. Do not label a lookup table, interpolation rule, fuzzy map, or operating-mode switch as adaptive unless closed-loop information changes its parameters or policy online.

12. Problems and Solutions

Problem 1 — Exact scalar schedule. For

\[ \dot x=(0.3+0.8\rho)x+2u, \qquad \dot x_m=-2x_m+2r, \]

derive \( \theta_s(\rho) \) and \( k_r \) in \( u=\theta_sx+k_rr \).

Solution. Matching requires

\[ 0.3+0.8\rho+2\theta_s=-2, \qquad 2k_r=2. \]

\[ \boxed{\theta_s(\rho)=-1.15-0.4\rho}, \qquad \boxed{k_r=1}. \]

The result is scheduled but not adaptive because no performance error updates the map.

Problem 2 — Scheduled mismatch bound. Suppose

\[ \dot e=-\lambda e+d(t), \qquad |d(t)|\le \bar d, \quad \lambda>0. \]

derive an ultimate bound for \( |e(t)| \).

Solution. Variation of constants gives

\[ e(t)=e^{-\lambda t}e(0)+\int_0^t e^{-\lambda(t-\tau)}d(\tau)d\tau. \]

\[ |e(t)|\le e^{-\lambda t}|e(0)|+ \bar d\frac{1-e^{-\lambda t} }{\lambda}. \]

Therefore

\[ \limsup_{t\rightarrow\infty}|e(t)|\le \frac{\bar d}{\lambda}. \]

In the scheduled plant, one may take \( d(t)=\Delta_a(\rho,t)x(t) \) after independently bounding \( x \).

Problem 3 — Lyapunov cancellation. For

\[ \dot e=-\lambda e+b\widetilde\theta\phi, \qquad b>0, \]

find an update law that makes \( \dot V=-\lambda e^2 \) for

\[ V=\frac12 e^2+\frac{b}{2\gamma}\widetilde\theta^2. \]

Solution.

\[ \dot V=-\lambda e^2+b\widetilde\theta\phi e+ \frac{b}{\gamma}\widetilde\theta\dot{\hat\theta}. \]

Choose

\[ \boxed{\dot{\hat\theta}=-\gamma\phi e}. \]

The two mixed terms cancel exactly.

Problem 4 — Why stable endpoints are insufficient. Two controller gains stabilize two grid-point models. Explain why linear interpolation of the gains is not by itself a proof for all intermediate conditions.

Solution. The closed-loop matrix generally depends on both the plant interpolation and controller interpolation. Eigenvalues are nonlinear functions of matrix entries, and stability is not preserved by arbitrary interpolation of stabilizing gains. In dynamic controllers, realization and controller-state consistency add further transition dynamics. A valid proof requires a certificate over the full parameter set, such as a common or parameter-dependent Lyapunov function, or a stability-preserving interpolation construction.

Problem 5 — Hybrid residual adaptation. Let

\[ a(\rho)=a_s(\rho)+\delta_a, \]

where \( \delta_a \) is an unknown constant. For \( \theta=\theta_s(\rho)+\hat\delta \), determine the ideal residual gain and prove tracking convergence.

Solution. The scheduled part cancels \( a_s(\rho) \), so

\[ \dot e=-\lambda e+(\delta_a+b\hat\delta)x. \]

Choose

\[ \delta^{\ast}=-\frac{\delta_a}{b}, \qquad \widetilde\delta=\hat\delta-\delta^{\ast}. \]

Then

\[ \dot e=-\lambda e+b\widetilde\delta x. \]

With

\[ \dot{\hat\delta}=-\gamma xe, \qquad V=\frac12e^2+\frac{b}{2\gamma}\widetilde\delta^2, \]

one obtains \( \dot V=-\lambda e^2 \). Boundedness and the usual Barbalat argument imply \( e(t)\rightarrow0 \).

13. Summary

Gain scheduling changes controller coefficients by evaluating an offline map of measured operating conditions. True adaptive control changes controller coefficients through dynamic update laws driven by closed-loop performance information. Scheduling is especially effective for known, repeatable envelope variation; adaptation is useful for unknown matched residuals, but introduces coupled state-parameter dynamics and additional robustness requirements. A scheduled nominal controller with bounded adaptive augmentation often provides the cleanest division of labor: immediate model-based compensation plus online correction of residual uncertainty.

14. References

  1. Shamma, J.S., & Athans, M. (1990). Analysis of gain scheduled control for nonlinear plants. IEEE Transactions on Automatic Control, 35(8), 898–907.
  2. Shamma, J.S., & Athans, M. (1991). Guaranteed properties of gain scheduled control for linear parameter-varying plants. Automatica, 27(3), 559–564.
  3. Shamma, J.S., & Athans, M. (1992). Gain scheduling: Potential hazards and possible remedies. IEEE Control Systems Magazine, 12(3), 101–107.
  4. Lawrence, D.A., & Rugh, W.J. (1995). Gain scheduling dynamic linear controllers for a nonlinear plant. Automatica, 31(3), 381–390.
  5. Rugh, W.J., & Shamma, J.S. (2000). Research on gain scheduling. Automatica, 36(10), 1401–1425.
  6. Leith, D.J., & Leithead, W.E. (2000). Survey of gain-scheduling analysis and design. International Journal of Control, 73(11), 1001–1025.
  7. Stilwell, D.J., & Rugh, W.J. (2000). Stability preserving interpolation methods for the synthesis of gain scheduled controllers. Automatica, 36(5), 665–671.
  8. Fromion, V., & Scorletti, G. (2003). A theoretical framework for gain scheduling. International Journal of Robust and Nonlinear Control, 13(10), 951–982.
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.