Chapter 1: Introduction to Adaptive Control

Lesson 1: Limitations of Fixed-Parameter Controllers

This lesson explains why controllers with fixed gains can perform well on a nominal model but fail to preserve the same transient response, tracking accuracy, robustness margins, or even stability when plant parameters change. We use only concepts from linear control: closed-loop poles, sensitivity, Routh-Hurwitz stability, model uncertainty, and simulation. Adaptive control will be motivated here, but not yet designed.

1. Why This Lesson Matters

In classical linear control, a controller is usually designed from a nominal plant model. Suppose the real plant belongs to a family indexed by an unknown parameter vector \( \boldsymbol{\theta} \in \Theta \):

\[ \dot{\mathbf{x}} = \mathbf{A}(\boldsymbol{\theta})\mathbf{x} + \mathbf{B}(\boldsymbol{\theta})u, \qquad y = \mathbf{C}(\boldsymbol{\theta})\mathbf{x}. \]

A fixed-parameter controller uses gains that do not change after implementation. For example, with state feedback \( u=-\mathbf{K}_0\mathbf{x}+k_r r \), the closed-loop matrix is

\[ \mathbf{A}_{cl}(\boldsymbol{\theta}) = \mathbf{A}(\boldsymbol{\theta}) - \mathbf{B}(\boldsymbol{\theta})\mathbf{K}_0. \]

The nominal design checks \( \mathbf{A}_{cl}(\boldsymbol{\theta}_0) \), but the real system evolves according to \( \mathbf{A}_{cl}(\boldsymbol{\theta}) \). Hence a fixed controller is successful only if the same gains remain acceptable over the actual uncertainty set.

flowchart TD
  A["Nominal model: theta0"] --> B["Design fixed gains K0"]
  B --> C["Nominal closed loop Acl(theta0)"]
  C --> D["Expected poles, damping, bandwidth"]
  E["Real plant: theta not equal theta0"] --> F["Actual closed loop Acl(theta)"]
  F --> G["Pole shift, gain change, tracking error"]
  G --> H["Possible oscillation, saturation, instability"]
  D --> I["Performance gap"]
  H --> I
        

2. Nominal Design Versus Actual Plant

Consider the scalar second-order plant used throughout this lesson:

\[ \ddot{y} + a\dot{y} + by = g u, \qquad a > 0,\; b > 0. \]

Here \( a \) is damping, \( b \) is stiffness-like restoring strength, and \( g \) is input gain. A fixed PD controller is

\[ u = k_p(r-y) + k_d(\dot{r}-\dot{y}). \]

For a constant reference, \( \dot{r}=0 \), the closed-loop equation becomes

\[ \ddot{y} + (a+gk_d)\dot{y} + (b+gk_p)y = gk_p r. \]

If the nominal model is \( (a_0,b_0,g_0) \), one can choose gains to match a desired second-order characteristic polynomial \( s^2+2\zeta_0\omega_0s+\omega_0^2 \):

\[ k_p = \frac{\omega_0^2-b_0}{g_0}, \qquad k_d = \frac{2\zeta_0\omega_0-a_0}{g_0}. \]

These gains are fixed. If the actual values \( (a,b,g) \) differ from \( (a_0,b_0,g_0) \), the actual closed-loop natural frequency and damping ratio become

\[ \omega_{cl}(a,b,g)=\sqrt{b+gk_p}, \qquad \zeta_{cl}(a,b,g)= \frac{a+gk_d}{2\sqrt{b+gk_p}}. \]

Therefore the same controller produces different transients when the plant gain, damping, or stiffness changes. This is not an adaptive effect; it is ordinary closed-loop pole migration under uncertainty.

3. Stability Is Parameter-Dependent

The characteristic polynomial of the closed-loop second-order system is

\[ p(s)=s^2+(a+gk_d)s+(b+gk_p). \]

For a second-order polynomial \( s^2+c_1s+c_0 \), the Routh-Hurwitz condition is \( c_1>0 \) and \( c_0>0 \). Hence the fixed PD controller is stabilizing only if

\[ a+gk_d > 0, \qquad b+gk_p > 0. \]

This condition exposes a fundamental limitation: gains selected from a nominal plant do not automatically stabilize every admissible plant. If \( g \) changes magnitude, performance changes; if the sign of \( g \) is unknown, even the direction of control action may be wrong.

Proof. Substitute the PD law into the plant:

\[ \ddot{y} + a\dot{y} + by = g\left(k_p(r-y)-k_d\dot{y}\right). \]

Collecting terms in \( y \) gives

\[ \ddot{y}+(a+gk_d)\dot{y}+(b+gk_p)y=gk_pr. \]

The homogeneous characteristic polynomial is \( p(s) \). By the second-order Routh-Hurwitz criterion, all roots satisfy \( \operatorname{Re}(s_i)<0 \) if and only if both coefficients are positive. Thus stability depends directly on the uncertain parameters \( a,b,g \).

4. Tracking Accuracy and Steady-State Error

For a constant reference \( r \), assume the closed loop is stable. At steady state, \( \dot{y}=\ddot{y}=0 \). The closed-loop equation gives

\[ (b+gk_p)y_{ss}=gk_pr. \]

Therefore

\[ y_{ss}=\frac{gk_p}{b+gk_p}r, \qquad e_{ss}=r-y_{ss}=\frac{b}{b+gk_p}r. \]

Increasing \( k_p \) reduces this error if \( g>0 \), but high proportional gain also increases control effort and can reduce robustness to unmodeled high-frequency dynamics. Fixed gains therefore create a trade-off: they can be tuned for one operating condition, but the same tuning may be conservative, aggressive, or unsafe elsewhere.

5. Robustness Viewpoint Using Sensitivity

In unity feedback with plant \( P(s) \) and fixed controller \( C(s) \), the sensitivity and complementary sensitivity functions are

\[ S(s)=\frac{1}{1+P(s)C(s)}, \qquad T(s)=\frac{P(s)C(s)}{1+P(s)C(s)}. \]

Small \( |S(j\omega)| \) is desirable for disturbance rejection and tracking at low frequency. Small \( |T(j\omega)| \) is desirable for measurement noise rejection and high-frequency robustness. A fixed controller cannot make both arbitrarily small over all frequencies, since \( S(s)+T(s)=1 \).

\[ S(s)+T(s)=\frac{1}{1+P(s)C(s)}+ \frac{P(s)C(s)}{1+P(s)C(s)}=1. \]

This classical sensitivity identity explains why simply increasing fixed gains is not a universal solution. The controller may improve low-frequency tracking while amplifying sensor noise, saturating actuators, or exciting neglected dynamics.

flowchart TD
  R["Reference r"] --> SUM["Error e = r - y"]
  SUM --> C["Fixed controller C(s)"]
  C --> U["Control input u"]
  U --> P["Real plant P(s, theta)"]
  P --> Y["Output y"]
  Y --> SUM
  N["Measurement noise n"] --> Y
  D["Disturbance d"] --> P
  P --> PERF["Performance depends on \ntheta, d, n, and \nunmodeled dynamics"]
        

6. A Small Impossibility Result for One Fixed Gain

Suppose the plant gain \( g \) varies in an interval \( g\in[g_{min},g_{max}] \) with \( 0<g_{min}<g_{max} \). For the PD-controlled second-order plant, the damping ratio is

\[ \zeta_{cl}(g)= \frac{a+gk_d}{2\sqrt{b+gk_p}}. \]

A designer may require both a minimum damping ratio and an upper bound on bandwidth:

\[ \zeta_{cl}(g)\ge \zeta_{min}, \qquad \omega_{cl}(g)=\sqrt{b+gk_p}\le \omega_{max}. \]

The bandwidth requirement imposes

\[ k_p \le \frac{\omega_{max}^2-b}{g_{max}}. \]

A small steady-state error requirement \( |e_{ss}/r|\le \varepsilon \) imposes

\[ \frac{b}{b+g_{min}k_p}\le \varepsilon \quad \Longrightarrow \quad k_p \ge \frac{b(1-\varepsilon)}{\varepsilon g_{min}}. \]

A feasible fixed proportional gain must satisfy both bounds. If

\[ \frac{b(1-\varepsilon)}{\varepsilon g_{min}} > \frac{\omega_{max}^2-b}{g_{max}}, \]

no single fixed value of \( k_p \) can satisfy both requirements over the entire gain range. This does not prove that adaptation is always necessary, but it proves that fixed-gain tuning can become structurally inadequate when uncertainty is large.

7. Practical Sources of Fixed-Controller Failure

Fixed-parameter controllers can fail or degrade for several reasons:

  • Parametric uncertainty: mass, inertia, damping, stiffness, actuator gain, aerodynamic derivative, or process gain is not equal to the nominal value.
  • Operating-point dependence: a controller tuned at one equilibrium may have poor damping or bandwidth at another equilibrium.
  • Time variation: payload, temperature, wear, or fluid level may change during operation.
  • Unmodeled dynamics: neglected flexible modes, delays, actuator dynamics, and sensor filters can be excited by high gains.
  • Disturbances and noise: fixed gains cannot distinguish parameter change from noise unless additional estimation or filtering structure is introduced.

These issues are the motivation for adaptive control. In later lessons, the controller will be allowed to update selected parameters online. At this point, the essential observation is simpler: fixed gains imply fixed assumptions about the plant.

8. Software Ecosystem for This Lesson

This lesson uses direct numerical simulation rather than specialized adaptive-control packages. The goal is to see fixed-controller limitations before designing adaptive laws. Useful tools include:

  • Python: NumPy for arrays, SciPy for ODE solvers, Matplotlib for plots, and python-control for transfer functions and state-space models.
  • C++: standard C++ for transparent implementation; Eigen for matrix computations; Boost.Odeint for larger ODE simulations.
  • Java: standard Java for transparent simulation; EJML or Apache Commons Math for matrix and numerical routines.
  • MATLAB/Simulink: ode45, Control System Toolbox, and Simulink block diagrams for dynamic-system simulation.
  • Wolfram Mathematica: NDSolve, symbolic manipulation, and state-space modeling.

9. Python Implementation

The Python implementation compares the nominal plant and an uncertain/time-varying plant under the same fixed PD controller.

Chapter1_Lesson1.py

"""
Chapter1_Lesson1.py

Lesson topic:
Limitations of fixed-parameter controllers under plant uncertainty.

This script simulates a nominal second-order plant and an uncertain/time-varying
plant controlled by the same fixed PD controller. The controller gains are chosen
from a nominal model. The comparison demonstrates that a controller that works
well for one set of plant parameters can degrade when plant gain and damping
change.

Dependencies:
    numpy
    matplotlib

Run:
    python Chapter1_Lesson1.py
"""

import numpy as np
import matplotlib.pyplot as plt


def nominal_gains(a0=0.40, b0=1.00, g0=1.00, omega_n=2.0, zeta=0.70):
    """Choose PD gains from the nominal closed-loop characteristic polynomial."""
    kp = (omega_n**2 - b0) / g0
    kd = (2.0 * zeta * omega_n - a0) / g0
    return kp, kd


def plant_parameters(t, case_name):
    """Return a(t), b(t), g(t) for nominal or uncertain plant."""
    if case_name == "nominal":
        return 0.40, 1.00, 1.00

    # Uncertain plant: damping slowly changes and actuator gain drops.
    a = 0.40 + 0.35 * np.sin(0.55 * t)
    b = 1.00
    g = 1.00 if t < 10.0 else 0.55
    return a, b, g


def reference(t):
    """Unit-step reference."""
    return 1.0


def rhs(t, x, kp, kd, case_name):
    """State derivative for x = [position, velocity]."""
    y, ydot = x
    a, b, g = plant_parameters(t, case_name)

    r = reference(t)
    rdot = 0.0

    # Fixed PD controller. It does not estimate or adapt to a(t), b(t), or g(t).
    u = kp * (r - y) + kd * (rdot - ydot)

    yddot = -a * ydot - b * y + g * u
    return np.array([ydot, yddot], dtype=float)


def rk4_step(f, t, x, h, *args):
    """One Runge-Kutta 4 integration step."""
    k1 = f(t, x, *args)
    k2 = f(t + 0.5 * h, x + 0.5 * h * k1, *args)
    k3 = f(t + 0.5 * h, x + 0.5 * h * k2, *args)
    k4 = f(t + h, x + h * k3, *args)
    return x + (h / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)


def simulate(case_name, t_final=25.0, dt=0.002):
    kp, kd = nominal_gains()
    time = np.arange(0.0, t_final + dt, dt)
    x = np.zeros((len(time), 2), dtype=float)
    u = np.zeros(len(time), dtype=float)

    for k in range(len(time) - 1):
        t = time[k]
        y, ydot = x[k]
        u[k] = kp * (reference(t) - y) - kd * ydot
        x[k + 1] = rk4_step(rhs, t, x[k], dt, kp, kd, case_name)

    u[-1] = kp * (reference(time[-1]) - x[-1, 0]) - kd * x[-1, 1]
    return time, x, u


def performance_metrics(time, y):
    r = np.ones_like(y)
    e = r - y
    iae = np.trapz(np.abs(e), time)
    ise = np.trapz(e**2, time)
    overshoot = max(0.0, np.max(y) - 1.0)
    final_error = e[-1]
    return {
        "IAE": iae,
        "ISE": ise,
        "Overshoot": overshoot,
        "FinalError": final_error,
    }


def main():
    kp, kd = nominal_gains()
    print(f"Fixed nominal gains: kp = {kp:.4f}, kd = {kd:.4f}")

    t_nom, x_nom, u_nom = simulate("nominal")
    t_unc, x_unc, u_unc = simulate("uncertain")

    print("Nominal metrics:", performance_metrics(t_nom, x_nom[:, 0]))
    print("Uncertain/time-varying metrics:", performance_metrics(t_unc, x_unc[:, 0]))

    np.savetxt(
        "Chapter1_Lesson1_python_results.csv",
        np.column_stack([t_nom, x_nom[:, 0], x_unc[:, 0], u_nom, u_unc]),
        delimiter=",",
        header="time,y_nominal,y_uncertain,u_nominal,u_uncertain",
        comments="",
    )

    plt.figure()
    plt.plot(t_nom, np.ones_like(t_nom), "--", label="reference")
    plt.plot(t_nom, x_nom[:, 0], label="nominal plant")
    plt.plot(t_unc, x_unc[:, 0], label="uncertain/time-varying plant")
    plt.xlabel("time [s]")
    plt.ylabel("output y(t)")
    plt.title("Fixed PD Controller: Nominal vs Uncertain Plant")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()
    plt.show()

    plt.figure()
    plt.plot(t_nom, u_nom, label="u(t), nominal")
    plt.plot(t_unc, u_unc, label="u(t), uncertain")
    plt.xlabel("time [s]")
    plt.ylabel("control input u(t)")
    plt.title("Control Effort with Fixed Gains")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    main()

10. C++ Implementation

The C++ version uses a from-scratch RK4 integrator and writes the response data to a CSV file.

Chapter1_Lesson1.cpp

/*
Chapter1_Lesson1.cpp

Lesson topic:
Limitations of fixed-parameter controllers under plant uncertainty.

This program simulates a second-order plant controlled by fixed nominal PD gains.
It writes a CSV file comparing nominal and uncertain/time-varying plants.

Build:
    g++ -std=c++17 -O2 Chapter1_Lesson1.cpp -o Chapter1_Lesson1

Run:
    ./Chapter1_Lesson1
*/

#include <array>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

struct Gains {
    double kp;
    double kd;
};

struct Params {
    double a;
    double b;
    double g;
};

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

Gains nominal_gains(double a0 = 0.40, double b0 = 1.00, double g0 = 1.00,
                    double omega_n = 2.0, double zeta = 0.70) {
    Gains gains;
    gains.kp = (omega_n * omega_n - b0) / g0;
    gains.kd = (2.0 * zeta * omega_n - a0) / g0;
    return gains;
}

Params plant_parameters(double t, const std::string& case_name) {
    if (case_name == "nominal") {
        return {0.40, 1.00, 1.00};
    }

    // Uncertain plant: damping slowly changes and actuator gain drops.
    double a = 0.40 + 0.35 * std::sin(0.55 * t);
    double b = 1.00;
    double g = (t < 10.0) ? 1.00 : 0.55;
    return {a, b, g};
}

double reference(double /*t*/) {
    return 1.0;
}

State rhs(double t, const State& x, const Gains& gains, const std::string& case_name) {
    const double y = x[0];
    const double ydot = x[1];
    Params p = plant_parameters(t, case_name);

    const double r = reference(t);
    const double rdot = 0.0;

    // Fixed PD controller. It does not estimate or adapt to a(t), b(t), or g(t).
    const double u = gains.kp * (r - y) + gains.kd * (rdot - ydot);
    const double yddot = -p.a * ydot - p.b * y + p.g * u;

    return {ydot, yddot};
}

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

State rk4_step(double t, const State& x, double h, const Gains& gains,
               const std::string& case_name) {
    State k1 = rhs(t, x, gains, case_name);
    State k2 = rhs(t + 0.5 * h, add_scaled(x, k1, 0.5 * h), gains, case_name);
    State k3 = rhs(t + 0.5 * h, add_scaled(x, k2, 0.5 * h), gains, case_name);
    State k4 = rhs(t + h, add_scaled(x, k3, h), gains, case_name);

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

struct Simulation {
    std::vector<double> time;
    std::vector<State> x;
    std::vector<double> u;
};

Simulation simulate(const std::string& case_name, double t_final = 25.0, double dt = 0.002) {
    Gains gains = nominal_gains();
    const int n = static_cast<int>(std::round(t_final / dt)) + 1;

    Simulation sim;
    sim.time.resize(n);
    sim.x.assign(n, {0.0, 0.0});
    sim.u.assign(n, 0.0);

    for (int k = 0; k < n - 1; ++k) {
        double t = k * dt;
        sim.time[k] = t;

        double y = sim.x[k][0];
        double ydot = sim.x[k][1];
        sim.u[k] = gains.kp * (reference(t) - y) - gains.kd * ydot;

        sim.x[k + 1] = rk4_step(t, sim.x[k], dt, gains, case_name);
    }

    sim.time[n - 1] = t_final;
    sim.u[n - 1] = gains.kp * (reference(t_final) - sim.x[n - 1][0]) - gains.kd * sim.x[n - 1][1];

    return sim;
}

double integral_absolute_error(const Simulation& sim) {
    double iae = 0.0;
    for (std::size_t k = 1; k < sim.time.size(); ++k) {
        double e0 = std::abs(1.0 - sim.x[k - 1][0]);
        double e1 = std::abs(1.0 - sim.x[k][0]);
        double dt = sim.time[k] - sim.time[k - 1];
        iae += 0.5 * (e0 + e1) * dt;
    }
    return iae;
}

int main() {
    Gains gains = nominal_gains();
    std::cout << std::fixed << std::setprecision(6);
    std::cout << "Fixed nominal gains: kp = " << gains.kp
              << ", kd = " << gains.kd << "\n";

    Simulation nominal = simulate("nominal");
    Simulation uncertain = simulate("uncertain");

    std::cout << "Nominal IAE: " << integral_absolute_error(nominal) << "\n";
    std::cout << "Uncertain/time-varying IAE: " << integral_absolute_error(uncertain) << "\n";

    std::ofstream file("Chapter1_Lesson1_cpp_results.csv");
    file << "time,y_nominal,y_uncertain,u_nominal,u_uncertain\n";
    for (std::size_t k = 0; k < nominal.time.size(); ++k) {
        file << nominal.time[k] << ","
             << nominal.x[k][0] << ","
             << uncertain.x[k][0] << ","
             << nominal.u[k] << ","
             << uncertain.u[k] << "\n";
    }

    std::cout << "Wrote Chapter1_Lesson1_cpp_results.csv\n";
    return 0;
}

11. Java Implementation

The Java version mirrors the C++ implementation and is intentionally dependency-free for teaching purposes.

Chapter1_Lesson1.java

/*
Chapter1_Lesson1.java

Lesson topic:
Limitations of fixed-parameter controllers under plant uncertainty.

This program simulates a second-order plant controlled by fixed nominal PD gains.
It writes a CSV file comparing nominal and uncertain/time-varying plants.

Build:
    javac Chapter1_Lesson1.java

Run:
    java Chapter1_Lesson1
*/

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

public class Chapter1_Lesson1 {
    static class Gains {
        final double kp;
        final double kd;

        Gains(double kp, double kd) {
            this.kp = kp;
            this.kd = kd;
        }
    }

    static class Params {
        final double a;
        final double b;
        final double g;

        Params(double a, double b, double g) {
            this.a = a;
            this.b = b;
            this.g = g;
        }
    }

    static class Simulation {
        final double[] time;
        final double[][] x;
        final double[] u;

        Simulation(int n) {
            time = new double[n];
            x = new double[n][2];
            u = new double[n];
        }
    }

    static Gains nominalGains() {
        double a0 = 0.40;
        double b0 = 1.00;
        double g0 = 1.00;
        double omegaN = 2.0;
        double zeta = 0.70;

        double kp = (omegaN * omegaN - b0) / g0;
        double kd = (2.0 * zeta * omegaN - a0) / g0;
        return new Gains(kp, kd);
    }

    static Params plantParameters(double t, String caseName) {
        if (caseName.equals("nominal")) {
            return new Params(0.40, 1.00, 1.00);
        }

        // Uncertain plant: damping slowly changes and actuator gain drops.
        double a = 0.40 + 0.35 * Math.sin(0.55 * t);
        double b = 1.00;
        double g = (t < 10.0) ? 1.00 : 0.55;
        return new Params(a, b, g);
    }

    static double reference(double t) {
        return 1.0;
    }

    static double[] rhs(double t, double[] x, Gains gains, String caseName) {
        double y = x[0];
        double ydot = x[1];
        Params p = plantParameters(t, caseName);

        double r = reference(t);
        double rdot = 0.0;

        // Fixed PD controller. It does not estimate or adapt to a(t), b(t), or g(t).
        double u = gains.kp * (r - y) + gains.kd * (rdot - ydot);
        double yddot = -p.a * ydot - p.b * y + p.g * u;

        return new double[] {ydot, yddot};
    }

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

    static double[] rk4Step(double t, double[] x, double h, Gains gains, String caseName) {
        double[] k1 = rhs(t, x, gains, caseName);
        double[] k2 = rhs(t + 0.5 * h, addScaled(x, k1, 0.5 * h), gains, caseName);
        double[] k3 = rhs(t + 0.5 * h, addScaled(x, k2, 0.5 * h), gains, caseName);
        double[] k4 = rhs(t + h, addScaled(x, k3, h), gains, caseName);

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

    static Simulation simulate(String caseName, double tFinal, double dt) {
        Gains gains = nominalGains();
        int n = (int)Math.round(tFinal / dt) + 1;
        Simulation sim = new Simulation(n);

        for (int k = 0; k < n - 1; k++) {
            double t = k * dt;
            sim.time[k] = t;

            double y = sim.x[k][0];
            double ydot = sim.x[k][1];
            sim.u[k] = gains.kp * (reference(t) - y) - gains.kd * ydot;

            double[] next = rk4Step(t, sim.x[k], dt, gains, caseName);
            sim.x[k + 1][0] = next[0];
            sim.x[k + 1][1] = next[1];
        }

        sim.time[n - 1] = tFinal;
        sim.u[n - 1] = gains.kp * (reference(tFinal) - sim.x[n - 1][0])
                     - gains.kd * sim.x[n - 1][1];

        return sim;
    }

    static double integralAbsoluteError(Simulation sim) {
        double iae = 0.0;
        for (int k = 1; k < sim.time.length; k++) {
            double e0 = Math.abs(1.0 - sim.x[k - 1][0]);
            double e1 = Math.abs(1.0 - sim.x[k][0]);
            double dt = sim.time[k] - sim.time[k - 1];
            iae += 0.5 * (e0 + e1) * dt;
        }
        return iae;
    }

    public static void main(String[] args) throws IOException {
        Locale.setDefault(Locale.US);

        Gains gains = nominalGains();
        System.out.printf("Fixed nominal gains: kp = %.6f, kd = %.6f%n", gains.kp, gains.kd);

        Simulation nominal = simulate("nominal", 25.0, 0.002);
        Simulation uncertain = simulate("uncertain", 25.0, 0.002);

        System.out.printf("Nominal IAE: %.6f%n", integralAbsoluteError(nominal));
        System.out.printf("Uncertain/time-varying IAE: %.6f%n", integralAbsoluteError(uncertain));

        try (PrintWriter out = new PrintWriter(new FileWriter("Chapter1_Lesson1_java_results.csv"))) {
            out.println("time,y_nominal,y_uncertain,u_nominal,u_uncertain");
            for (int k = 0; k < nominal.time.length; k++) {
                out.printf("%.8f,%.8f,%.8f,%.8f,%.8f%n",
                    nominal.time[k],
                    nominal.x[k][0],
                    uncertain.x[k][0],
                    nominal.u[k],
                    uncertain.u[k]);
            }
        }

        System.out.println("Wrote Chapter1_Lesson1_java_results.csv");
    }
}

12. MATLAB/Simulink Implementation

The MATLAB script simulates the time-varying plant with ode45. If Simulink is available, it also creates a simple block-diagram model comparing a nominal and a weakened fixed plant with the same fixed controller.

Chapter1_Lesson1.m

% Chapter1_Lesson1.m
%
% Lesson topic:
% Limitations of fixed-parameter controllers under plant uncertainty.
%
% This script simulates a nominal second-order plant and an uncertain/time-varying
% plant controlled by the same fixed PD controller. The controller gains are
% chosen from the nominal model.
%
% Requirements:
%   MATLAB base functions: ode45, plot
%   Optional: Control System Toolbox for additional LTI analysis.
%   Optional: Simulink for the block-diagram model generated at the end.
%
% Run:
%   Chapter1_Lesson1

clear; clc; close all;

a0 = 0.40;
b0 = 1.00;
g0 = 1.00;
omega_n = 2.0;
zeta = 0.70;

kp = (omega_n^2 - b0) / g0;
kd = (2*zeta*omega_n - a0) / g0;

fprintf('Fixed nominal gains: kp = %.6f, kd = %.6f\n', kp, kd);

tspan = [0 25];
x0 = [0; 0];

[tNom, xNom] = ode45(@(t, x) plant_rhs(t, x, kp, kd, "nominal"), tspan, x0);
[tUnc, xUnc] = ode45(@(t, x) plant_rhs(t, x, kp, kd, "uncertain"), tspan, x0);

rNom = ones(size(tNom));
rUnc = ones(size(tUnc));

iaeNom = trapz(tNom, abs(rNom - xNom(:, 1)));
iaeUnc = trapz(tUnc, abs(rUnc - xUnc(:, 1)));

fprintf('Nominal IAE: %.6f\n', iaeNom);
fprintf('Uncertain/time-varying IAE: %.6f\n', iaeUnc);

figure;
plot(tNom, rNom, '--', 'LineWidth', 1.2); hold on;
plot(tNom, xNom(:, 1), 'LineWidth', 1.2);
plot(tUnc, xUnc(:, 1), 'LineWidth', 1.2);
grid on;
xlabel('time [s]');
ylabel('output y(t)');
title('Fixed PD Controller: Nominal vs Uncertain Plant');
legend('reference', 'nominal plant', 'uncertain/time-varying plant', 'Location', 'best');

uNom = kp*(1 - xNom(:, 1)) - kd*xNom(:, 2);
uUnc = kp*(1 - xUnc(:, 1)) - kd*xUnc(:, 2);

figure;
plot(tNom, uNom, 'LineWidth', 1.2); hold on;
plot(tUnc, uUnc, 'LineWidth', 1.2);
grid on;
xlabel('time [s]');
ylabel('control input u(t)');
title('Control Effort with Fixed Gains');
legend('u(t), nominal', 'u(t), uncertain', 'Location', 'best');

writematrix([tNom, xNom(:, 1), uNom], 'Chapter1_Lesson1_matlab_nominal_results.csv');
writematrix([tUnc, xUnc(:, 1), uUnc], 'Chapter1_Lesson1_matlab_uncertain_results.csv');

% Optional Simulink model:
% This creates a block-diagram comparison using two fixed LTI plants:
% (1) the nominal plant and (2) a weakened plant with lower actuator gain.
% The controller is identical in both loops. A filtered derivative is used so
% the controller transfer function is proper.
if exist('new_system', 'file') == 4 || exist('new_system', 'file') == 2
    try
        create_fixed_pd_simulink_model(kp, kd);
        fprintf('Created Simulink model: Chapter1_Lesson1_Simulink_FixedPD.slx\n');
    catch ME
        fprintf('Simulink model creation skipped: %s\n', ME.message);
    end
end

function dx = plant_rhs(t, x, kp, kd, caseName)
    y = x(1);
    ydot = x(2);

    [a, b, g] = plant_parameters(t, caseName);

    r = 1.0;
    rdot = 0.0;

    % Fixed PD controller. It does not estimate or adapt to a(t), b(t), or g(t).
    u = kp*(r - y) + kd*(rdot - ydot);

    yddot = -a*ydot - b*y + g*u;
    dx = [ydot; yddot];
end

function [a, b, g] = plant_parameters(t, caseName)
    if caseName == "nominal"
        a = 0.40;
        b = 1.00;
        g = 1.00;
    else
        % Uncertain plant: damping slowly changes and actuator gain drops.
        a = 0.40 + 0.35*sin(0.55*t);
        b = 1.00;
        if t < 10.0
            g = 1.00;
        else
            g = 0.55;
        end
    end
end

function create_fixed_pd_simulink_model(kp, kd)
    model = 'Chapter1_Lesson1_Simulink_FixedPD';
    if bdIsLoaded(model)
        close_system(model, 0);
    end

    new_system(model);
    open_system(model);

    N = 30;  % derivative filter bandwidth
    controller_num = [kp + kd*N, kp*N];
    controller_den = [1, N];

    add_block('simulink/Sources/Step', [model '/Step'], ...
        'Time', '0', 'Before', '0', 'After', '1', ...
        'Position', [30 120 60 150]);

    add_block('simulink/Math Operations/Sum', [model '/SumNominal'], ...
        'Inputs', '+-', 'Position', [110 95 135 125]);
    add_block('simulink/Continuous/Transfer Fcn', [model '/FixedPD_Nominal'], ...
        'Numerator', mat2str(controller_num), ...
        'Denominator', mat2str(controller_den), ...
        'Position', [170 90 270 130]);
    add_block('simulink/Continuous/Transfer Fcn', [model '/Plant_Nominal'], ...
        'Numerator', mat2str([1.00]), ...
        'Denominator', mat2str([1 0.40 1.00]), ...
        'Position', [320 90 430 130]);

    add_block('simulink/Math Operations/Sum', [model '/SumWeakened'], ...
        'Inputs', '+-', 'Position', [110 215 135 245]);
    add_block('simulink/Continuous/Transfer Fcn', [model '/FixedPD_Weakened'], ...
        'Numerator', mat2str(controller_num), ...
        'Denominator', mat2str(controller_den), ...
        'Position', [170 210 270 250]);
    add_block('simulink/Continuous/Transfer Fcn', [model '/Plant_Weakened'], ...
        'Numerator', mat2str([0.55]), ...
        'Denominator', mat2str([1 0.75 1.00]), ...
        'Position', [320 210 430 250]);

    add_block('simulink/Signal Routing/Mux', [model '/Mux'], ...
        'Inputs', '3', 'Position', [500 135 515 235]);
    add_block('simulink/Sinks/Scope', [model '/Scope'], ...
        'Position', [570 155 620 215]);

    add_line(model, 'Step/1', 'SumNominal/1', 'autorouting', 'on');
    add_line(model, 'SumNominal/1', 'FixedPD_Nominal/1', 'autorouting', 'on');
    add_line(model, 'FixedPD_Nominal/1', 'Plant_Nominal/1', 'autorouting', 'on');
    add_line(model, 'Plant_Nominal/1', 'SumNominal/2', 'autorouting', 'on');

    add_line(model, 'Step/1', 'SumWeakened/1', 'autorouting', 'on');
    add_line(model, 'SumWeakened/1', 'FixedPD_Weakened/1', 'autorouting', 'on');
    add_line(model, 'FixedPD_Weakened/1', 'Plant_Weakened/1', 'autorouting', 'on');
    add_line(model, 'Plant_Weakened/1', 'SumWeakened/2', 'autorouting', 'on');

    add_line(model, 'Plant_Nominal/1', 'Mux/1', 'autorouting', 'on');
    add_line(model, 'Plant_Weakened/1', 'Mux/2', 'autorouting', 'on');
    add_line(model, 'Step/1', 'Mux/3', 'autorouting', 'on');
    add_line(model, 'Mux/1', 'Scope/1', 'autorouting', 'on');

    set_param(model, 'StopTime', '25');
    save_system(model);
end

13. Wolfram Mathematica Implementation

The Mathematica version uses NDSolve to compare nominal and uncertain closed-loop responses.

Chapter1_Lesson1.nb

(* Chapter1_Lesson1.nb *)

ClearAll["Global`*"];

a0 = 0.40;
b0 = 1.00;
g0 = 1.00;
omegaN = 2.0;
zeta = 0.70;

kp = (omegaN^2 - b0)/g0;
kd = (2 zeta omegaN - a0)/g0;

plantParameters[t_, "nominal"] := {0.40, 1.00, 1.00};
plantParameters[t_, "uncertain"] := {0.40 + 0.35 Sin[0.55 t], 1.00, If[t < 10.0, 1.00, 0.55]};

simulate[caseName_] := Module[{a, b, g, sol},
  sol = NDSolve[
    {
      y''[t] == -(plantParameters[t, caseName][[1]]) y'[t]
                -(plantParameters[t, caseName][[2]]) y[t]
                +(plantParameters[t, caseName][[3]]) (kp (1 - y[t]) - kd y'[t]),
      y[0] == 0,
      y'[0] == 0
    },
    y,
    {t, 0, 25}
  ];
  sol
];

nominalSol = simulate["nominal"];
uncertainSol = simulate["uncertain"];

Plot[
  {1, Evaluate[y[t] /. nominalSol], Evaluate[y[t] /. uncertainSol]},
  {t, 0, 25},
  PlotLegends -> {"reference", "nominal plant", "uncertain/time-varying plant"},
  AxesLabel -> {"time [s]", "output y(t)"},
  PlotLabel -> "Fixed PD Controller: Nominal vs Uncertain Plant",
  GridLines -> Automatic
]

iaeNominal = NIntegrate[Abs[1 - Evaluate[y[t] /. nominalSol][[1]]], {t, 0, 25}];
iaeUncertain = NIntegrate[Abs[1 - Evaluate[y[t] /. uncertainSol][[1]]], {t, 0, 25}];

{kp, kd, iaeNominal, iaeUncertain}

14. Problems and Solutions

Problem 1 (Closed-Loop Polynomial): For \( \ddot{y}+a\dot{y}+by=gu \) and \( u=k_p(r-y)-k_d\dot{y} \), derive the closed-loop characteristic polynomial.

Solution: Substitute the controller into the plant:

\[ \ddot{y}+a\dot{y}+by = g\left(k_p r-k_p y-k_d\dot{y}\right). \]

Move the terms containing \( y \) to the left:

\[ \ddot{y}+(a+gk_d)\dot{y}+(b+gk_p)y=gk_pr. \]

The homogeneous characteristic polynomial is therefore \( p(s)=s^2+(a+gk_d)s+(b+gk_p) \).

Problem 2 (Nominal Gain Selection): Let \( a_0=0.4 \), \( b_0=1 \), \( g_0=1 \). Choose \( k_p,k_d \) so the nominal characteristic polynomial is \( s^2+2\zeta_0\omega_0s+\omega_0^2 \) with \( \zeta_0=0.7 \) and \( \omega_0=2 \).

Solution: Matching coefficients gives

\[ b_0+g_0k_p=\omega_0^2, \qquad a_0+g_0k_d=2\zeta_0\omega_0. \]

Therefore

\[ k_p=\frac{4-1}{1}=3, \qquad k_d=\frac{2(0.7)(2)-0.4}{1}=2.4. \]

Problem 3 (Stability Under Reduced Input Gain): Using \( k_p=3 \), \( k_d=2.4 \), \( a=0.4 \), \( b=1 \), find the closed-loop polynomial when \( g=0.55 \). Is it stable?

Solution:

\[ p(s)=s^2+(0.4+0.55\cdot 2.4)s+(1+0.55\cdot 3). \]

\[ p(s)=s^2+1.72s+2.65. \]

Both coefficients are positive, so the second-order Routh-Hurwitz criterion gives stability. However, the transient response is not the nominal one because \( \omega_{cl}=\sqrt{2.65} \) and \( \zeta_{cl}=1.72/(2\sqrt{2.65}) \), not the designed values \( 2 \) and \( 0.7 \).

Problem 4 (Steady-State Error): For the same plant and fixed PD controller, compute the unit-step steady-state error.

Solution: At steady state, derivatives vanish:

\[ (b+gk_p)y_{ss}=gk_p. \]

Hence

\[ e_{ss}=1-y_{ss}= 1-\frac{gk_p}{b+gk_p} =\frac{b}{b+gk_p}. \]

For \( b=1 \), \( g=1 \), \( k_p=3 \), the error is \( 1/4=0.25 \). For \( g=0.55 \), the error is \( 1/(1+1.65)\approx 0.377 \). Lower input gain worsens tracking even though the same controller is still stable.

Problem 5 (Fixed-Gain Feasibility): Let \( b=1 \), \( g_{min}=0.5 \), \( g_{max}=2 \), \( \varepsilon=0.1 \), and \( \omega_{max}=3 \). Can one fixed \( k_p \) guarantee both \( |e_{ss}|\le 0.1|r| \) and \( \omega_{cl}\le 3 \) for all \( g\in[0.5,2] \)?

Solution: The error bound requires

\[ k_p\ge \frac{1(1-0.1)}{0.1(0.5)}=18. \]

The bandwidth bound requires

\[ k_p\le \frac{3^2-1}{2}=4. \]

Since \( 18>4 \), the requirements are contradictory. No single fixed proportional gain can satisfy both specifications over the entire uncertainty interval.

15. Summary

Fixed-parameter controllers are designed for an assumed plant. When the plant parameters change, the closed-loop poles, damping ratio, natural frequency, steady-state error, control effort, and robustness margins also change. The key limitation is not that fixed controllers are useless; rather, they are tied to a fixed model. Adaptive control begins from the idea that some controller parameters should be adjusted online when uncertainty is structured and measurable signals contain useful information about that uncertainty.

16. References

  1. Whitaker, H.P., Yamron, J., & Kezer, A. (1958). Design of Model Reference Adaptive Control Systems for Aircraft. Massachusetts Institute of Technology Instrumentation Laboratory Report R-164.
  2. Osburn, P.V., Whitaker, H.P., & Kezer, A. (1961). New developments in the design of model reference adaptive control systems. Institute of Radio Engineers Transactions on Automatic Control, 6(1), 14–20.
  3. Parks, P.C. (1966). Liapunov redesign of model reference adaptive control systems. IEEE Transactions on Automatic Control, 11(3), 362–367.
  4. Monopoli, R.V. (1974). Model reference adaptive control with an augmented error signal. IEEE Transactions on Automatic Control, 19(5), 474–484.
  5. Narendra, K.S., Lin, Y.H., & Valavani, L.S. (1980). Stable adaptive controller design, Part II: Proof of stability. IEEE Transactions on Automatic Control, 25(3), 440–448.
  6. Morse, A.S. (1980). Global stability of parameter-adaptive control systems. IEEE Transactions on Automatic Control, 25(3), 433–439.
  7. Ioannou, P.A., & Kokotovic, P.V. (1984). Instability analysis and improvement of robustness of adaptive control. Automatica, 20(5), 583–594.
  8. Rohrs, C.E., Valavani, L.S., Athans, M., & Stein, G. (1985). Robustness of continuous-time adaptive control algorithms in the presence of unmodeled dynamics. IEEE Transactions on Automatic Control, 30(9), 881–889.
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.