Chapter 18: Adaptive Control of Nonlinear Systems

Lesson 5: Limitations and Need for Structural Knowledge of Nonlinearities

Adaptive control does not remove the need for a model class. It replaces exact numerical knowledge of selected quantities with online adjustment, while retaining assumptions about relative degree, control direction, uncertainty location, parameterization, measurable signals, and internal dynamics. This lesson makes those assumptions explicit, proves where the standard Lyapunov cancellation succeeds, and shows what remains when the assumed nonlinear structure is incomplete or wrong.

1. Learning Objectives and Scope

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

  • distinguish unknown parameter values from unknown model structure;
  • state the structural assumptions behind a nonlinear adaptive law;
  • derive the exact Lyapunov cancellation for matched, linearly parameterized uncertainty;
  • derive an ultimate tracking-error bound when an approximation residual remains;
  • explain why wrong relative degree, unknown control direction, unmatched uncertainty, or unstable zero dynamics cannot be repaired by merely increasing adaptation gain;
  • audit a nonlinear plant before selecting MRAC, adaptive backstepping, approximation-based adaptation, robust adaptation, or a nonadaptive redesign.

The central distinction is between parametric uncertainty and structural uncertainty. If the plant belongs to a known family \( \mathcal{P}=\{P(\theta):\theta\in\Theta\} \), adaptation may estimate or compensate \( \theta \). If the true plant lies outside \( \mathcal{P} \), the parameter estimator is being asked to explain dynamics that its regressor cannot represent.

2. Adaptation Requires a Known Uncertainty Channel

Consider the control-affine nonlinear system

\[ \dot{\mathbf{x}} = \mathbf{f}_0(\mathbf{x}) + \mathbf{Y}(\mathbf{x})\boldsymbol{\theta} + \mathbf{g}(\mathbf{x})u + \boldsymbol{\Delta}(\mathbf{x},t), \qquad y=h(\mathbf{x}). \]

The entries of \( \boldsymbol{\theta} \) are unknown constants, but the functions \( \mathbf{f}_0 \), \( \mathbf{Y} \), \( \mathbf{g} \), and \( h \) define the assumed structure. The term \( \boldsymbol{\Delta} \) contains everything outside that model class: neglected dynamics, unknown delays, hysteresis, unmodeled couplings, approximation residuals, or disturbances.

A parameter update law can cancel terms proportional to the parameter error only when the plant-controller mismatch admits a known factorization. For linear parameterization,

\[ \mathbf{Y}(\mathbf{x})\boldsymbol{\theta} - \mathbf{Y}(\mathbf{x})\hat{\boldsymbol{\theta}} = \mathbf{Y}(\mathbf{x})\tilde{\boldsymbol{\theta}}, \qquad \tilde{\boldsymbol{\theta}} = \boldsymbol{\theta}-\hat{\boldsymbol{\theta}}. \]

This identity is not a minor algebraic convenience; it is the mechanism that makes Lyapunov cross-term cancellation possible. Adaptation changes coefficients inside a chosen basis. It does not discover arbitrary state dimension, relative degree, actuator dynamics, or zero dynamics without additional architecture and assumptions.

3. Structural Assumptions Commonly Hidden Inside Nonlinear Adaptive Designs

flowchart TD
  P["Nonlinear plant"] --> A["Known state and input channels"]
  A --> B["Known relative degree"]
  B --> C["Known nonzero control direction"]
  C --> D["Uncertainty has matched \nor recursive structure"]
  D --> E["Unknown terms admit \na usable parameterization"]
  E --> F["Internal dynamics are \nstable or stabilizable"]
  F --> G["Signals remain in \nthe validity region"]
  G --> H["Lyapunov adaptive design is justified"]
  B --> X["If false: redesign model and sensing"]
  C --> X
  D --> X
  E --> X
  F --> X
        

The most consequential assumptions are:

  1. Known relative degree. The number of differentiations from output to input must match the controller derivation.
  2. Known control direction. The sign or orientation of the high-frequency gain must be known, unless a special mechanism such as the Nussbaum construction from Chapter 16 is used.
  3. Nonvanishing input effectiveness. The input gain must not cross zero in the operating region.
  4. Matched uncertainty or recursive strict-feedback structure. The unknown term must enter through a channel the controller can influence directly or through a known backstepping recursion.
  5. Usable parameterization. The uncertainty must be linear in unknown constants, or satisfy an alternative condition such as monotonicity, convexity, known bounds, or a finite approximation model.
  6. Minimum-phase or stable internal dynamics. Output tracking does not guarantee bounded hidden states when the zero dynamics are unstable.
  7. Availability of required signals. State-feedback proofs do not automatically transfer to output feedback.
  8. Validity domain. Function approximators and local coordinate transformations generally provide guarantees only on a specified compact set.

4. Exact Lyapunov Cancellation Under Correct Structure

Consider the scalar nonlinear plant

\[ \dot{x}=\boldsymbol{\theta}^{T}\boldsymbol{\phi}(x)+u, \qquad \boldsymbol{\phi}(x)= \begin{bmatrix}\phi_1(x)&\cdots&\phi_p(x)\end{bmatrix}^{T}, \]

where the basis functions are known and the constant parameter vector is unknown. Let \( r(t) \) be differentiable and define \( e=x-r \). Choose

\[ u=-\hat{\boldsymbol{\theta}}^{T}\boldsymbol{\phi}(x) +\dot{r}-ke, \qquad k>0. \]

The tracking-error dynamics become

\[ \dot{e}=-ke+\tilde{\boldsymbol{\theta}}^{T} \boldsymbol{\phi}(x). \]

Use the Lyapunov candidate

\[ V=\frac{1}{2}e^2+ \frac{1}{2}\tilde{\boldsymbol{\theta}}^{T} \boldsymbol{\Gamma}^{-1}\tilde{\boldsymbol{\theta}}, \qquad \boldsymbol{\Gamma}=\boldsymbol{\Gamma}^{T}>0. \]

Because the true parameters are constant,

\[ \dot{\tilde{\boldsymbol{\theta}}}=-\dot{\hat{\boldsymbol{\theta}}}, \]

and therefore

\[ \dot{V}=-ke^2+e\tilde{\boldsymbol{\theta}}^{T}\boldsymbol{\phi} -\tilde{\boldsymbol{\theta}}^{T}\boldsymbol{\Gamma}^{-1} \dot{\hat{\boldsymbol{\theta}}}. \]

Select the adaptive law

\[ \dot{\hat{\boldsymbol{\theta}}} =\boldsymbol{\Gamma}\boldsymbol{\phi}(x)e. \]

Then the mixed term cancels exactly:

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

Hence \( e \) and \( \tilde{\boldsymbol{\theta}} \) are bounded. If the closed-loop signals make \( \dot e \) bounded, Barbalat's lemma gives \( e(t)\rightarrow 0 \). Parameter convergence still requires persistent excitation; structural correctness alone does not identify the true parameter vector.

5. What Remains When the Model Class Is Incomplete

Suppose the true error equation contains an unmatched residual:

\[ \dot{e}=-ke+\tilde{\boldsymbol{\theta}}^{T}\boldsymbol{\phi}(x) +\Delta(x,t), \qquad |\Delta(x,t)|\leq \bar{\Delta}. \]

With the same update law,

\[ \dot{V}=-ke^2+e\Delta. \]

Young's inequality gives, for any \( \varepsilon>0 \),

\[ e\Delta\leq \frac{\varepsilon}{2}e^2+ \frac{1}{2\varepsilon}\bar{\Delta}^{2}, \]

so that

\[ \dot{V}\leq -\left(k-\frac{\varepsilon}{2}\right)e^2 +\frac{\bar{\Delta}^{2}}{2\varepsilon}. \]

For \( 0<\varepsilon<2k \), the derivative is negative whenever

\[ |e|>\frac{\bar{\Delta}} {\sqrt{2\varepsilon\left(k-\varepsilon/2\right)}}. \]

Thus the proof generally yields uniform ultimate boundedness, not asymptotic tracking. Increasing \( k \) can shrink this idealized bound, but high feedback and adaptation bandwidth may excite neglected dynamics, amplify noise, or saturate actuators. A mathematical residual bound must therefore be accompanied by a physically credible bandwidth model.

6. Failure Modes Caused by Wrong Structural Knowledge

Structural errorWhy the standard proof failsTypical response
Wrong relative degreeThe assumed input-output error equation is incorrect; differentiating the output reveals additional states or delays.Identify actuator/sensor dynamics, change the controller order, or use dynamic extension.
Unknown or changing input directionThe stabilizing control sign may become destabilizing.Use prior sign knowledge, a Chapter 16 Nussbaum-type design, switching logic, or redesign the actuator interface.
Unmatched uncertaintyThe unknown term is not multiplied by the same input vector field, so direct cancellation is impossible.Exploit strict-feedback structure, robust bounds, disturbance observers, or accept an ultimate bound.
Unstable zero dynamicsOutput tracking can coexist with divergence of hidden internal states.Verify minimum-phase conditions or redesign the regulated output.
Unmodeled fast dynamicsAdaptive high gain can excite neglected poles and create phase loss.Reduce bandwidth, add robust modification, filter regressors, and include actuator/sensor models.
Nonlinear parameterizationThe uncertainty difference is not a known regressor times a parameter error.Use monotonicity, convexity, overparameterization, switching, or a problem-specific transformation.
Unknown state dimension or hidden modeNo finite parameter vector in the assumed model reproduces the plant trajectories.Perform model validation and system identification before adaptive-law tuning.
Approximation outside training domainThe residual bound used in the Lyapunov proof is no longer valid.Constrain the operating set, update the basis/domain, or provide a safety controller.

7. Why Nonlinear Parameterization Is Fundamentally Different

For the uncertainty

\[ f(x,\theta)=\sin(\theta x), \]

the estimation mismatch is

\[ \sin(\theta x)-\sin(\hat{\theta}x). \]

By the mean-value theorem, for some intermediate value \( \xi \) between \( \theta \) and \( \hat\theta \),

\[ \sin(\theta x)-\sin(\hat{\theta}x) =x\cos(\xi x)(\theta-\hat\theta). \]

The coefficient \( x\cos(\xi x) \) depends on the unknown intermediate value. It is therefore not the known regressor required by the simple gradient cancellation. A valid design needs extra structure, for example a known monotonicity sign, a convex/concave parameter dependence, a compact parameter set, a triangular form, a switching estimator, or an alternative embedding into a linearly parameterized model.

Local Taylor expansion is not a free solution. Around \( \hat\theta \),

\[ f(x,\theta)=f(x,\hat\theta)+ \frac{\partial f}{\partial\theta}(x,\hat\theta)\tilde\theta+ R_2(x,\theta,\hat\theta), \]

and the second-order remainder \( R_2 \) becomes another structural residual. A local adaptive law must keep the state and estimate inside the region in which that remainder is bounded.

8. An Indistinguishability Argument: Why Arbitrary Unknown Nonlinearities Cannot Be Universally Adapted

Consider a causal controller that chooses \( u(t) \) from the past measured input-output history. Suppose the admitted plant class contains two systems \( P_1 \) and \( P_2 \) that generate the same measured history on \( [0,T] \), but require opposite control action immediately after \( T \) to remain stable. At time \( T \), the controller's information is identical for both plants, so causality forces it to choose the same control value for both. That value cannot be the required stabilizing action for both systems.

The contradiction shows that a universal guarantee requires restrictions that rule out such indistinguishable adversarial pairs. In adaptive-control language, those restrictions are the structural assumptions: known sign, known relative degree, bounded uncertainty class, identifiability, excitation, or a known dynamic normal form.

This argument does not say that data-driven or learning controllers are useless. It says that their guarantees also depend on a hypothesis class, data coverage, regularity assumptions, safety constraints, or online exploration conditions. The structural knowledge may be encoded in physics, basis functions, neural architecture, priors, filters, or a backup controller, but it does not disappear.

9. Function Approximation Moves the Assumptions; It Does Not Eliminate Them

On a compact set \( \Omega \), an unknown continuous nonlinearity may be represented as

\[ f(\mathbf{x})=\mathbf{W}^{*T}\boldsymbol{\sigma}(\mathbf{x}) +\varepsilon(\mathbf{x}), \qquad \sup_{\mathbf{x}\in\Omega}|\varepsilon(\mathbf{x})|\leq\bar\varepsilon. \]

Adapting \( \hat{\mathbf W} \) restores linearity in the unknown weights, but the following knowledge remains necessary:

  • the state or feature vector supplied to the approximator;
  • the compact domain \( \Omega \) in which approximation quality is certified;
  • a bound or robust treatment for \( \varepsilon \);
  • the control-affine channel and its direction;
  • stable internal dynamics and sufficient smoothness;
  • a mechanism that prevents the state from leaving the approximation domain.

With a nonzero residual, the Lyapunov result is usually practical stability or an ultimate bound. Exact asymptotic cancellation requires the target function to lie in the span of the chosen basis, or an additional robust term whose own assumptions are satisfied.

10. Structural Audit Before Designing an Adaptive Controller

flowchart TD
  S["Define measured output and control objective"] --> R["Determine relative degree and hidden dynamics"]
  R --> G["Verify input gain is nonzero and its direction is known"]
  G --> U["Classify uncertainty: matched, recursive, unmatched, dynamic"]
  U --> P["Test parameterization: linear, monotone, bounded approximation"]
  P --> Z["Check zero dynamics and state availability"]
  Z --> V["State the validity region and residual bounds"]
  V --> C["Choose adaptive and robust modifications"]
  C --> T["Stress test noise, delay, saturation, and unmodeled poles"]
  T --> Q["Accept proof domain or revise the model class"]
        

A defensible design report should explicitly answer:

  1. Which plant functions are known exactly, and which coefficients are unknown?
  2. Where does every uncertain term enter the state equations?
  3. Which identity produces the parameter-error regressor in the Lyapunov derivative?
  4. What residual remains after the adaptive cancellation?
  5. What bound is assumed for that residual, and over what state domain?
  6. What prevents actuator saturation, hidden dynamics, or noise from invalidating the proof?
  7. What is monitored online to detect departure from the assumed model class?

11. Comparative Case Study: Correct Parameterization vs Hidden Actuator Dynamics

The nominal plant is

\[ \dot{x}=\theta_1x+\theta_2x^3+u. \]

With \( \boldsymbol\phi=[x\;x^3]^T \), use

\[ u_c=-\hat\theta_1x-\hat\theta_2x^3+\dot r-k(x-r), \]

\[ \dot{\hat\theta}_1=\gamma_1x(x-r), \qquad \dot{\hat\theta}_2=\gamma_2x^3(x-r). \]

The mismatched plant contains an actuator state omitted from the controller model:

\[ \dot{x}=\theta_1x+\theta_2x^3+z+d(t), \qquad \tau\dot z=-z+u_c. \]

The adaptive law treats tracking error caused by \( z-u_c \) and \( d(t) \) as though it were parameter error. Consequently, the estimates may move away from the physical coefficients while attempting to absorb phase lag and disturbance. This is not necessarily an estimator bug; it is a model set-membership failure.

The following implementations integrate both models and report RMS tracking error. The expected qualitative result is better tracking for the matched model, with more biased or oscillatory parameter estimates in the hidden-actuator case.

12. Python Implementation

Chapter18_Lesson5.py

# Chapter18_Lesson5.py
# Structural mismatch experiment for nonlinear adaptive control.
# Dependencies: numpy, matplotlib

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import csv
import math
import numpy as np
import matplotlib.pyplot as plt


@dataclass
class Configuration:
    dt: float = 0.001
    final_time: float = 20.0
    theta1: float = 0.8
    theta2: float = -0.4
    feedback_gain: float = 3.0
    gamma1: float = 4.0
    gamma2: float = 1.5
    actuator_time_constant: float = 0.12
    leakage: float = 0.0
    state_limit: float = 25.0


def reference(t: float) -> tuple[float, float]:
    return 0.6 * math.sin(0.5 * t), 0.3 * math.cos(0.5 * t)


def dynamics(t: float, state: np.ndarray, cfg: Configuration, hidden_actuator: bool) -> np.ndarray:
    x, z, theta1_hat, theta2_hat = state
    r, r_dot = reference(t)
    error = x - r
    phi1 = x
    phi2 = x**3

    commanded_input = (
        -theta1_hat * phi1
        -theta2_hat * phi2
        + r_dot
        - cfg.feedback_gain * error
    )

    theta1_hat_dot = cfg.gamma1 * phi1 * error - cfg.leakage * theta1_hat
    theta2_hat_dot = cfg.gamma2 * phi2 * error - cfg.leakage * theta2_hat

    if hidden_actuator:
        # The controller assumes that commanded_input acts directly on x.
        # The true plant instead contains an unmodeled first-order actuator z.
        z_dot = (-z + commanded_input) / cfg.actuator_time_constant
        disturbance = 0.08 * math.sin(4.0 * t)
        x_dot = cfg.theta1 * x + cfg.theta2 * x**3 + z + disturbance
    else:
        z_dot = 0.0
        x_dot = cfg.theta1 * x + cfg.theta2 * x**3 + commanded_input

    return np.array([x_dot, z_dot, theta1_hat_dot, theta2_hat_dot], dtype=float)


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


def simulate(cfg: Configuration, hidden_actuator: bool) -> dict[str, np.ndarray]:
    count = int(round(cfg.final_time / cfg.dt)) + 1
    time = np.linspace(0.0, cfg.final_time, count)
    state = np.array([0.25, 0.0, 0.0, 0.0], dtype=float)

    x_history = np.zeros(count)
    z_history = np.zeros(count)
    theta1_history = np.zeros(count)
    theta2_history = np.zeros(count)
    reference_history = np.zeros(count)
    input_history = np.zeros(count)

    for index, t in enumerate(time):
        x, z, theta1_hat, theta2_hat = state
        r, r_dot = reference(t)
        error = x - r
        commanded_input = (
            -theta1_hat * x
            -theta2_hat * x**3
            + r_dot
            - cfg.feedback_gain * error
        )

        x_history[index] = x
        z_history[index] = z
        theta1_history[index] = theta1_hat
        theta2_history[index] = theta2_hat
        reference_history[index] = r
        input_history[index] = commanded_input

        if abs(x) > cfg.state_limit or not np.isfinite(state).all():
            x_history[index:] = np.nan
            z_history[index:] = np.nan
            theta1_history[index:] = np.nan
            theta2_history[index:] = np.nan
            input_history[index:] = np.nan
            break

        if index + 1 < count:
            state = rk4_step(t, state, cfg, hidden_actuator)

    return {
        "time": time,
        "reference": reference_history,
        "x": x_history,
        "z": z_history,
        "theta1_hat": theta1_history,
        "theta2_hat": theta2_history,
        "commanded_input": input_history,
    }


def rms_tracking_error(result: dict[str, np.ndarray]) -> float:
    error = result["x"] - result["reference"]
    return float(np.sqrt(np.nanmean(error**2)))


def write_csv(path: Path, matched: dict[str, np.ndarray], hidden: dict[str, np.ndarray]) -> None:
    with path.open("w", newline="", encoding="utf-8") as stream:
        writer = csv.writer(stream)
        writer.writerow([
            "time", "reference", "x_matched", "x_hidden",
            "theta1_hat_matched", "theta2_hat_matched",
            "theta1_hat_hidden", "theta2_hat_hidden",
            "commanded_input_matched", "commanded_input_hidden",
        ])
        for values in zip(
            matched["time"], matched["reference"], matched["x"], hidden["x"],
            matched["theta1_hat"], matched["theta2_hat"],
            hidden["theta1_hat"], hidden["theta2_hat"],
            matched["commanded_input"], hidden["commanded_input"],
        ):
            writer.writerow(values)


def create_plots(path: Path, matched: dict[str, np.ndarray], hidden: dict[str, np.ndarray]) -> None:
    time = matched["time"]

    plt.figure(figsize=(10, 5))
    plt.plot(time, matched["reference"], label="reference")
    plt.plot(time, matched["x"], label="matched model")
    plt.plot(time, hidden["x"], label="hidden actuator")
    plt.xlabel("Time [s]")
    plt.ylabel("State / reference")
    plt.title("Tracking performance under correct and incorrect structure")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()
    plt.savefig(path, dpi=160)
    plt.close()


def main() -> None:
    cfg = Configuration()
    matched = simulate(cfg, hidden_actuator=False)
    hidden = simulate(cfg, hidden_actuator=True)

    output_directory = Path(__file__).resolve().parent
    write_csv(output_directory / "Chapter18_Lesson5_results.csv", matched, hidden)
    create_plots(output_directory / "Chapter18_Lesson5_tracking.png", matched, hidden)

    print(f"Matched-model RMS error: {rms_tracking_error(matched):.6f}")
    print(f"Hidden-actuator RMS error: {rms_tracking_error(hidden):.6f}")
    print("Generated Chapter18_Lesson5_results.csv and Chapter18_Lesson5_tracking.png")


if __name__ == "__main__":
    main()

Libraries: NumPy supplies vector arithmetic and Matplotlib produces the comparison plot. The controller and fourth-order Runge-Kutta integrator are implemented explicitly so that the adaptive dynamics remain visible.

13. C++ Implementation

Chapter18_Lesson5.cpp

// Chapter18_Lesson5.cpp
// Structural mismatch experiment for nonlinear adaptive control.
// Build: g++ -std=c++17 -O2 Chapter18_Lesson5.cpp -o Chapter18_Lesson5

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

struct Configuration {
    double dt = 0.001;
    double final_time = 20.0;
    double theta1 = 0.8;
    double theta2 = -0.4;
    double feedback_gain = 3.0;
    double gamma1 = 4.0;
    double gamma2 = 1.5;
    double actuator_time_constant = 0.12;
    double leakage = 0.0;
    double state_limit = 25.0;
};

using State = std::array<double, 4>;  // x, z, theta1_hat, theta2_hat

struct Result {
    std::vector<double> time;
    std::vector<double> reference;
    std::vector<double> x;
    std::vector<double> theta1_hat;
    std::vector<double> theta2_hat;
    std::vector<double> commanded_input;
};

std::array<double, 2> reference_signal(double t) {
    return {0.6 * std::sin(0.5 * t), 0.3 * std::cos(0.5 * t)};
}

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

State dynamics(double t, const State& state, const Configuration& cfg, bool hidden_actuator) {
    const double x = state[0];
    const double z = state[1];
    const double theta1_hat = state[2];
    const double theta2_hat = state[3];
    const auto ref = reference_signal(t);
    const double error = x - ref[0];
    const double phi1 = x;
    const double phi2 = x * x * x;

    const double commanded_input = -theta1_hat * phi1 - theta2_hat * phi2
        + ref[1] - cfg.feedback_gain * error;

    const double theta1_hat_dot = cfg.gamma1 * phi1 * error - cfg.leakage * theta1_hat;
    const double theta2_hat_dot = cfg.gamma2 * phi2 * error - cfg.leakage * theta2_hat;

    double x_dot = 0.0;
    double z_dot = 0.0;
    if (hidden_actuator) {
        z_dot = (-z + commanded_input) / cfg.actuator_time_constant;
        const double disturbance = 0.08 * std::sin(4.0 * t);
        x_dot = cfg.theta1 * x + cfg.theta2 * phi2 + z + disturbance;
    } else {
        x_dot = cfg.theta1 * x + cfg.theta2 * phi2 + commanded_input;
    }

    return {x_dot, z_dot, theta1_hat_dot, theta2_hat_dot};
}

State rk4_step(double t, const State& state, const Configuration& cfg, bool hidden_actuator) {
    const State k1 = dynamics(t, state, cfg, hidden_actuator);
    const State k2 = dynamics(t + 0.5 * cfg.dt, add(state, k1, 0.5 * cfg.dt), cfg, hidden_actuator);
    const State k3 = dynamics(t + 0.5 * cfg.dt, add(state, k2, 0.5 * cfg.dt), cfg, hidden_actuator);
    const State k4 = dynamics(t + cfg.dt, add(state, k3, cfg.dt), cfg, hidden_actuator);

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

Result simulate(const Configuration& cfg, bool hidden_actuator) {
    const std::size_t count = static_cast<std::size_t>(std::llround(cfg.final_time / cfg.dt)) + 1U;
    Result result;
    result.time.resize(count);
    result.reference.resize(count);
    result.x.resize(count);
    result.theta1_hat.resize(count);
    result.theta2_hat.resize(count);
    result.commanded_input.resize(count);

    State state{0.25, 0.0, 0.0, 0.0};
    for (std::size_t i = 0; i < count; ++i) {
        const double t = static_cast<double>(i) * cfg.dt;
        const auto ref = reference_signal(t);
        const double x = state[0];
        const double error = x - ref[0];
        const double input = -state[2] * x - state[3] * x * x * x
            + ref[1] - cfg.feedback_gain * error;

        result.time[i] = t;
        result.reference[i] = ref[0];
        result.x[i] = x;
        result.theta1_hat[i] = state[2];
        result.theta2_hat[i] = state[3];
        result.commanded_input[i] = input;

        if (!std::isfinite(x) || std::abs(x) > cfg.state_limit) {
            const double nan = std::numeric_limits<double>::quiet_NaN();
            for (std::size_t j = i; j < count; ++j) {
                result.x[j] = nan;
                result.theta1_hat[j] = nan;
                result.theta2_hat[j] = nan;
                result.commanded_input[j] = nan;
            }
            break;
        }

        if (i + 1U < count) {
            state = rk4_step(t, state, cfg, hidden_actuator);
        }
    }
    return result;
}

double rms_error(const Result& result) {
    double sum = 0.0;
    std::size_t count = 0U;
    for (std::size_t i = 0; i < result.time.size(); ++i) {
        if (std::isfinite(result.x[i])) {
            const double error = result.x[i] - result.reference[i];
            sum += error * error;
            ++count;
        }
    }
    if (count == 0U) {
        throw std::runtime_error("No finite simulation samples were produced.");
    }
    return std::sqrt(sum / static_cast<double>(count));
}

void write_csv(const Result& matched, const Result& hidden) {
    std::ofstream stream("Chapter18_Lesson5_results_cpp.csv");
    if (!stream) {
        throw std::runtime_error("Could not create output CSV file.");
    }
    stream << std::setprecision(12);
    stream << "time,reference,x_matched,x_hidden,theta1_hat_matched,theta2_hat_matched,"
              "theta1_hat_hidden,theta2_hat_hidden,input_matched,input_hidden\n";
    for (std::size_t i = 0; i < matched.time.size(); ++i) {
        stream << matched.time[i] << ',' << matched.reference[i] << ','
               << matched.x[i] << ',' << hidden.x[i] << ','
               << matched.theta1_hat[i] << ',' << matched.theta2_hat[i] << ','
               << hidden.theta1_hat[i] << ',' << hidden.theta2_hat[i] << ','
               << matched.commanded_input[i] << ',' << hidden.commanded_input[i] << '\n';
    }
}

int main() {
    try {
        const Configuration cfg;
        const Result matched = simulate(cfg, false);
        const Result hidden = simulate(cfg, true);
        write_csv(matched, hidden);
        std::cout << std::fixed << std::setprecision(6)
                  << "Matched-model RMS error: " << rms_error(matched) << '\n'
                  << "Hidden-actuator RMS error: " << rms_error(hidden) << '\n'
                  << "Generated Chapter18_Lesson5_results_cpp.csv\n";
        return 0;
    } catch (const std::exception& error) {
        std::cerr << "Error: " << error.what() << '\n';
        return 1;
    }
}

The C++ version uses only the standard library and writes a CSV file suitable for plotting in GNUplot, Python, MATLAB, or a spreadsheet application.

14. Java Implementation

Chapter18_Lesson5.java

// Chapter18_Lesson5.java
// Structural mismatch experiment for nonlinear adaptive control.
// Build and run: javac Chapter18_Lesson5.java && java Chapter18_Lesson5

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

public final class Chapter18_Lesson5 {
    private static final class Configuration {
        final double dt = 0.001;
        final double finalTime = 20.0;
        final double theta1 = 0.8;
        final double theta2 = -0.4;
        final double feedbackGain = 3.0;
        final double gamma1 = 4.0;
        final double gamma2 = 1.5;
        final double actuatorTimeConstant = 0.12;
        final double leakage = 0.0;
        final double stateLimit = 25.0;
    }

    private static final class Result {
        final double[] time;
        final double[] reference;
        final double[] x;
        final double[] theta1Hat;
        final double[] theta2Hat;
        final double[] commandedInput;

        Result(int count) {
            time = new double[count];
            reference = new double[count];
            x = new double[count];
            theta1Hat = new double[count];
            theta2Hat = new double[count];
            commandedInput = new double[count];
        }
    }

    private static double[] referenceSignal(double t) {
        return new double[]{0.6 * Math.sin(0.5 * t), 0.3 * Math.cos(0.5 * t)};
    }

    private static double[] dynamics(
            double t, double[] state, Configuration cfg, boolean hiddenActuator) {
        double x = state[0];
        double z = state[1];
        double theta1Hat = state[2];
        double theta2Hat = state[3];
        double[] reference = referenceSignal(t);
        double error = x - reference[0];
        double phi1 = x;
        double phi2 = x * x * x;

        double commandedInput = -theta1Hat * phi1 - theta2Hat * phi2
                + reference[1] - cfg.feedbackGain * error;
        double theta1HatDot = cfg.gamma1 * phi1 * error - cfg.leakage * theta1Hat;
        double theta2HatDot = cfg.gamma2 * phi2 * error - cfg.leakage * theta2Hat;

        double xDot;
        double zDot;
        if (hiddenActuator) {
            zDot = (-z + commandedInput) / cfg.actuatorTimeConstant;
            double disturbance = 0.08 * Math.sin(4.0 * t);
            xDot = cfg.theta1 * x + cfg.theta2 * phi2 + z + disturbance;
        } else {
            zDot = 0.0;
            xDot = cfg.theta1 * x + cfg.theta2 * phi2 + commandedInput;
        }
        return new double[]{xDot, zDot, theta1HatDot, theta2HatDot};
    }

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

    private static double[] rk4Step(
            double t, double[] state, Configuration cfg, boolean hiddenActuator) {
        double[] k1 = dynamics(t, state, cfg, hiddenActuator);
        double[] k2 = dynamics(t + 0.5 * cfg.dt, add(state, k1, 0.5 * cfg.dt), cfg, hiddenActuator);
        double[] k3 = dynamics(t + 0.5 * cfg.dt, add(state, k2, 0.5 * cfg.dt), cfg, hiddenActuator);
        double[] k4 = dynamics(t + cfg.dt, add(state, k3, cfg.dt), cfg, hiddenActuator);

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

    private static Result simulate(Configuration cfg, boolean hiddenActuator) {
        int count = (int) Math.round(cfg.finalTime / cfg.dt) + 1;
        Result result = new Result(count);
        double[] state = new double[]{0.25, 0.0, 0.0, 0.0};

        for (int i = 0; i < count; ++i) {
            double t = i * cfg.dt;
            double[] reference = referenceSignal(t);
            double x = state[0];
            double error = x - reference[0];
            double input = -state[2] * x - state[3] * x * x * x
                    + reference[1] - cfg.feedbackGain * error;

            result.time[i] = t;
            result.reference[i] = reference[0];
            result.x[i] = x;
            result.theta1Hat[i] = state[2];
            result.theta2Hat[i] = state[3];
            result.commandedInput[i] = input;

            if (!Double.isFinite(x) || Math.abs(x) > cfg.stateLimit) {
                for (int j = i; j < count; ++j) {
                    result.x[j] = Double.NaN;
                    result.theta1Hat[j] = Double.NaN;
                    result.theta2Hat[j] = Double.NaN;
                    result.commandedInput[j] = Double.NaN;
                }
                break;
            }

            if (i + 1 < count) {
                state = rk4Step(t, state, cfg, hiddenActuator);
            }
        }
        return result;
    }

    private static double rmsError(Result result) {
        double sum = 0.0;
        int count = 0;
        for (int i = 0; i < result.time.length; ++i) {
            if (Double.isFinite(result.x[i])) {
                double error = result.x[i] - result.reference[i];
                sum += error * error;
                ++count;
            }
        }
        if (count == 0) {
            throw new IllegalStateException("No finite simulation samples were produced.");
        }
        return Math.sqrt(sum / count);
    }

    private static void writeCsv(Result matched, Result hidden) throws IOException {
        Path path = Path.of("Chapter18_Lesson5_results_java.csv");
        try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
            writer.write("time,reference,x_matched,x_hidden,theta1_hat_matched,theta2_hat_matched,");
            writer.write("theta1_hat_hidden,theta2_hat_hidden,input_matched,input_hidden\n");
            for (int i = 0; i < matched.time.length; ++i) {
                writer.write(String.format(Locale.ROOT,
                        "%.12g,%.12g,%.12g,%.12g,%.12g,%.12g,%.12g,%.12g,%.12g,%.12g%n",
                        matched.time[i], matched.reference[i], matched.x[i], hidden.x[i],
                        matched.theta1Hat[i], matched.theta2Hat[i],
                        hidden.theta1Hat[i], hidden.theta2Hat[i],
                        matched.commandedInput[i], hidden.commandedInput[i]));
            }
        }
    }

    public static void main(String[] args) {
        try {
            Configuration cfg = new Configuration();
            Result matched = simulate(cfg, false);
            Result hidden = simulate(cfg, true);
            writeCsv(matched, hidden);
            System.out.printf(Locale.ROOT, "Matched-model RMS error: %.6f%n", rmsError(matched));
            System.out.printf(Locale.ROOT, "Hidden-actuator RMS error: %.6f%n", rmsError(hidden));
            System.out.println("Generated Chapter18_Lesson5_results_java.csv");
        } catch (IOException | RuntimeException error) {
            System.err.println("Error: " + error.getMessage());
            System.exit(1);
        }
    }
}

The Java implementation uses standard arrays and NIO file output. It intentionally mirrors the C++ equations so students can compare numerical implementation details rather than different control laws.

15. MATLAB and Simulink Implementation

Chapter18_Lesson5.m

% Chapter18_Lesson5.m
% Structural mismatch experiment for nonlinear adaptive control.
% The script uses ode45 and requires only base MATLAB.

clear; clc; close all;

cfg.theta1 = 0.8;
cfg.theta2 = -0.4;
cfg.k = 3.0;
cfg.gamma1 = 4.0;
cfg.gamma2 = 1.5;
cfg.tau = 0.12;
cfg.leakage = 0.0;
cfg.stateLimit = 25.0;

tspan = linspace(0, 20, 20001);
y0 = [0.25; 0.0; 0.0; 0.0]; % x, z, theta1_hat, theta2_hat
options = odeset('RelTol', 1e-8, 'AbsTol', 1e-10, ...
    'Events', @(t,y) stateLimitEvent(t, y, cfg));

[tMatched, yMatched] = ode45(@(t,y) closedLoop(t, y, cfg, false), tspan, y0, options);
[tHidden, yHidden] = ode45(@(t,y) closedLoop(t, y, cfg, true), tspan, y0, options);

rMatched = 0.6 * sin(0.5 * tMatched);
rHidden = 0.6 * sin(0.5 * tHidden);
rmsMatched = sqrt(mean((yMatched(:,1) - rMatched).^2));
rmsHidden = sqrt(mean((yHidden(:,1) - rHidden).^2));

fprintf('Matched-model RMS error: %.6f\n', rmsMatched);
fprintf('Hidden-actuator RMS error: %.6f\n', rmsHidden);

figure('Name', 'Structural knowledge experiment');
plot(tMatched, rMatched, 'LineWidth', 1.3); hold on;
plot(tMatched, yMatched(:,1), 'LineWidth', 1.1);
plot(tHidden, yHidden(:,1), 'LineWidth', 1.1);
grid on;
xlabel('Time [s]');
ylabel('State / reference');
title('Tracking under correct and incorrect structural assumptions');
legend('Reference', 'Matched model', 'Hidden actuator', 'Location', 'best');

figure('Name', 'Adaptive parameters');
plot(tMatched, yMatched(:,3), 'LineWidth', 1.1); hold on;
plot(tMatched, yMatched(:,4), 'LineWidth', 1.1);
plot(tHidden, yHidden(:,3), '--', 'LineWidth', 1.1);
plot(tHidden, yHidden(:,4), '--', 'LineWidth', 1.1);
grid on;
xlabel('Time [s]');
ylabel('Parameter estimate');
title('Parameter estimates absorb structural mismatch');
legend('theta1 hat matched', 'theta2 hat matched', ...
    'theta1 hat hidden', 'theta2 hat hidden', 'Location', 'best');

matchedTable = table(tMatched, rMatched, yMatched(:,1), yMatched(:,3), yMatched(:,4), ...
    'VariableNames', {'time','reference','x','theta1_hat','theta2_hat'});
hiddenTable = table(tHidden, rHidden, yHidden(:,1), yHidden(:,3), yHidden(:,4), ...
    'VariableNames', {'time','reference','x','theta1_hat','theta2_hat'});
writetable(matchedTable, 'Chapter18_Lesson5_matched_matlab.csv');
writetable(hiddenTable, 'Chapter18_Lesson5_hidden_matlab.csv');

% Simulink realization:
% 1. Use Integrator blocks for x, z, theta1_hat, and theta2_hat.
% 2. Build phi = [x; x^3] with Product blocks.
% 3. Implement u_c = -theta_hat' * phi + r_dot - k * (x-r).
% 4. For the matched case, connect u_c directly to the plant input.
% 5. For the mismatched case, insert Transfer Fcn 1/(tau*s+1) before the plant.
% 6. Compare tracking error and parameter estimates with Scope blocks.

function dy = closedLoop(t, y, cfg, hiddenActuator)
    x = y(1);
    z = y(2);
    theta1Hat = y(3);
    theta2Hat = y(4);

    r = 0.6 * sin(0.5 * t);
    rDot = 0.3 * cos(0.5 * t);
    e = x - r;
    phi1 = x;
    phi2 = x^3;
    uCommand = -theta1Hat * phi1 - theta2Hat * phi2 + rDot - cfg.k * e;

    theta1HatDot = cfg.gamma1 * phi1 * e - cfg.leakage * theta1Hat;
    theta2HatDot = cfg.gamma2 * phi2 * e - cfg.leakage * theta2Hat;

    if hiddenActuator
        zDot = (-z + uCommand) / cfg.tau;
        disturbance = 0.08 * sin(4.0 * t);
        xDot = cfg.theta1 * x + cfg.theta2 * x^3 + z + disturbance;
    else
        zDot = 0.0;
        xDot = cfg.theta1 * x + cfg.theta2 * x^3 + uCommand;
    end

    dy = [xDot; zDot; theta1HatDot; theta2HatDot];
end

function [value, isterminal, direction] = stateLimitEvent(~, y, cfg)
    value = cfg.stateLimit - abs(y(1));
    isterminal = 1;
    direction = -1;
end

The script includes a block-level Simulink realization. The key experiment is to insert 1/(tau*s+1) between the commanded input and the nonlinear plant, while leaving the adaptive controller unchanged. The resulting difference isolates structural mismatch from ordinary parameter uncertainty.

16. Wolfram Mathematica Implementation

Chapter18_Lesson5.nb


(* Chapter18_Lesson5.nb *)
Notebook[{
  Cell["Chapter 18, Lesson 5: Structural Knowledge Experiment", "Title"],
  Cell["Evaluate the following cell to compare a correctly parameterized nonlinear adaptive controller with a plant containing an unmodeled actuator state.", "Text"],
  Cell[BoxData[RowBox[{"ToExpression", "[", "\"ClearAll[\"Global`*\"];
cfg = <|\"theta1\" -> 0.8, \"theta2\" -> -0.4, \"k\" -> 3.0,
  \"gamma1\" -> 4.0, \"gamma2\" -> 1.5, \"tau\" -> 0.12,
  \"leakage\" -> 0.0, \"finalTime\" -> 20.0|>;
reference[t_] := 0.6 Sin[0.5 t]; referenceDot[t_] := 0.3 Cos[0.5 t];
simulate[hiddenActuator_] := Module[{equations, solution, input, disturbance},
  input[t_] := -theta1Hat[t] x[t] - theta2Hat[t] x[t]^3
    + referenceDot[t] - cfg[\"k\"] (x[t] - reference[t]);
  disturbance[t_] := If[hiddenActuator, 0.08 Sin[4.0 t], 0.0];
  equations = {
    x'[t] == cfg[\"theta1\"] x[t] + cfg[\"theta2\"] x[t]^3
      + If[hiddenActuator, z[t], input[t]] + disturbance[t],
    z'[t] == If[hiddenActuator, (-z[t] + input[t])/cfg[\"tau\"], 0.0],
    theta1Hat'[t] == cfg[\"gamma1\"] x[t] (x[t] - reference[t])
      - cfg[\"leakage\"] theta1Hat[t],
    theta2Hat'[t] == cfg[\"gamma2\"] x[t]^3 (x[t] - reference[t])
      - cfg[\"leakage\"] theta2Hat[t],
    x[0] == 0.25, z[0] == 0.0,
    theta1Hat[0] == 0.0, theta2Hat[0] == 0.0
  };
  NDSolveValue[equations, {x, z, theta1Hat, theta2Hat},
    {t, 0.0, cfg[\"finalTime\"]},
    Method -> {\"TimeIntegration\" ->
      {\"ExplicitRungeKutta\", \"DifferenceOrder\" -> 4}},
    MaxStepFraction -> 1/2000]
];
matched = simulate[False]; hidden = simulate[True];
timeGrid = Subdivide[0.0, cfg[\"finalTime\"], 4000];
matchedX = matched[[1]] /@ timeGrid; hiddenX = hidden[[1]] /@ timeGrid;
referenceValues = reference /@ timeGrid;
rmsMatched = Sqrt[Mean[(matchedX - referenceValues)^2]];
rmsHidden = Sqrt[Mean[(hiddenX - referenceValues)^2]];
Print[\"Matched-model RMS error: \", NumberForm[rmsMatched, {8, 6}]];
Print[\"Hidden-actuator RMS error: \", NumberForm[rmsHidden, {8, 6}]];
trackingPlot = ListLinePlot[
  {Transpose[{timeGrid, referenceValues}], Transpose[{timeGrid, matchedX}],
    Transpose[{timeGrid, hiddenX}]},
  PlotLegends -> {\"Reference\", \"Matched model\", \"Hidden actuator\"},
  Frame -> True, FrameLabel -> {\"Time [s]\", \"State / reference\"},
  PlotLabel -> \"Tracking under correct and incorrect structural assumptions\",
  ImageSize -> Large];
parameterPlot = ListLinePlot[
  {Transpose[{timeGrid, matched[[3]] /@ timeGrid}],
    Transpose[{timeGrid, matched[[4]] /@ timeGrid}],
    Transpose[{timeGrid, hidden[[3]] /@ timeGrid}],
    Transpose[{timeGrid, hidden[[4]] /@ timeGrid}]},
  PlotLegends -> {\"theta1 hat matched\", \"theta2 hat matched\",
    \"theta1 hat hidden\", \"theta2 hat hidden\"},
  Frame -> True, FrameLabel -> {\"Time [s]\", \"Parameter estimate\"},
  PlotLabel -> \"Parameter estimates absorb structural mismatch\",
  ImageSize -> Large];
Export[\"Chapter18_Lesson5_tracking_mathematica.png\", trackingPlot];
Export[\"Chapter18_Lesson5_parameters_mathematica.png\", parameterPlot];
Column[{trackingPlot, parameterPlot}]
\"", "]"}]], "Input"]
}, WindowTitle -> "Chapter18_Lesson5", StyleDefinitions -> "Default.nb"]        

The notebook contains an executable input cell that calls ToExpression on the full Mathematica implementation, solves both closed-loop models with NDSolveValue, and exports the plots.

17. Interpretation and Engineering Consequences

  1. Tracking convergence is not proof of parameter correctness. Even under the correct model, non-PE trajectories permit multiple parameter vectors to produce the same motion.
  2. Parameter drift can be a structural diagnostic. Persistent estimate motion correlated with actuator lag, saturation, or unmodeled frequency content suggests that the regressor is absorbing omitted physics.
  3. High adaptation gain is not a substitute for model validation. It increases estimator bandwidth and may amplify precisely the dynamics excluded from the nominal model.
  4. Robust modifications bound symptoms, not every cause. Projection, leakage, normalization, and dead zones can limit estimates and noise sensitivity, but they do not restore a false relative-degree or minimum-phase assumption.
  5. Hybrid architectures are often appropriate. A physics-based nominal controller, bounded adaptive correction, disturbance estimation, saturation logic, and supervisory safety monitor allocate different uncertainty classes to different mechanisms.

18. Problems and Solutions

Problem 1 (Exact cancellation): For \( \dot x=\theta_1x+\theta_2x^3+u \), derive a controller and adaptation law that make the derivative of \( V=\tfrac12e^2+\tfrac{1}{2\gamma_1}\tilde\theta_1^2+ \tfrac{1}{2\gamma_2}\tilde\theta_2^2 \) equal to \( -ke^2 \).

Solution: Choose

\[ u=-\hat\theta_1x-\hat\theta_2x^3+\dot r-ke. \]

Then

\[ \dot e=-ke+\tilde\theta_1x+\tilde\theta_2x^3. \]

The Lyapunov derivative is

\[ \dot V=-ke^2+e\tilde\theta_1x+e\tilde\theta_2x^3 -\frac{1}{\gamma_1}\tilde\theta_1\dot{\hat\theta}_1 -\frac{1}{\gamma_2}\tilde\theta_2\dot{\hat\theta}_2. \]

Set

\[ \dot{\hat\theta}_1=\gamma_1xe, \qquad \dot{\hat\theta}_2=\gamma_2x^3e, \]

which gives \( \dot V=-ke^2 \).

Problem 2 (Ultimate bound): Suppose the error dynamics of Problem 1 include a residual \( \Delta \) with \( |\Delta|\leq 0.2 \). With \( k=4 \) and \( \varepsilon=4 \), compute the radius outside which the derived Lyapunov upper bound is negative.

Solution: The sufficient radius is

\[ \rho=\frac{\bar\Delta}{ \sqrt{2\varepsilon(k-\varepsilon/2)}} =\frac{0.2}{\sqrt{2(4)(4-2)}}=0.05. \]

This is a sufficient analytical bound, not an exact steady-state error prediction.

Problem 3 (Unmatched uncertainty): Consider

\[ \dot x_1=x_2+\theta x_1^2, \qquad \dot x_2=u. \]

Explain why the uncertainty is not directly matched with the input and identify the structural method already introduced in the course that can exploit this system.

Solution: The input acts in the second equation, whereas the unknown term appears in the first equation. A one-step cancellation at the physical input cannot remove \( \theta x_1^2 \). However, the system has strict-feedback structure: \( x_2 \) can be treated as a virtual control for the first subsystem, and adaptive backstepping from Chapter 17 can recursively construct the virtual and actual controls.

Problem 4 (Nonlinear parameterization): For \( f(x,\theta)=\exp(\theta x) \), show why \( f(x,\theta)-f(x,\hat\theta) \) is not a known regressor times \( \tilde\theta \).

Solution: The mean-value theorem gives

\[ \exp(\theta x)-\exp(\hat\theta x) =x\exp(\xi x)(\theta-\hat\theta), \]

where \( \xi \) lies between the unknown true and estimated parameters. The multiplier \( x\exp(\xi x) \) is not known. A valid adaptive design therefore needs additional monotonicity/bounding information, a transformed parameterization, or a robust residual treatment.

Problem 5 (Hidden actuator): The controller is derived for \( \dot x=f(x)+u_c \), but the true actuator satisfies \( \tau\dot z=-z+u_c \) and the plant is \( \dot x=f(x)+z \). Derive the mismatch term appearing in the nominal error equation.

Solution: Add and subtract \( u_c \):

\[ \dot x=f(x)+u_c+(z-u_c). \]

The nominal adaptive proof now contains \( \Delta_a=z-u_c \). Since \( \Delta_a \) depends dynamically on the command and actuator state, it is not a constant parametric error. A small-\( \tau \) approximation may justify treating it as bounded only after establishing command smoothness and actuator stability.

Problem 6 (Structural audit): A neural approximator achieves a small validation error on \( |x|\leq 2 \), but the adaptive closed loop has no invariant-set proof. Is the universal-approximation statement sufficient for stability?

Solution: No. The approximation-error bound is certified only on a compact domain. Without proving that the closed-loop state remains in that domain, the Lyapunov inequality may cease to hold. A complete design needs an invariant-set argument, state constraints/barriers, a backup controller, or approximation guarantees over the reachable region.

19. Summary

Nonlinear adaptive control is powerful when uncertainty belongs to a known structural family. The standard Lyapunov proof depends on a known regressor multiplying a parameter error, a valid input channel, stable internal dynamics, and bounded signals. Structural residuals change asymptotic cancellation into practical bounds and can cause parameter drift, high-frequency excitation, or instability. Neural, fuzzy, and other universal approximators enlarge the model class but still require a domain, residual bound, input structure, and safety mechanism. The correct engineering sequence is therefore: establish structure, classify uncertainty, derive the residual, state the guarantee domain, and only then tune the adaptive law.

20. References

  1. Rohrs, C.E., Valavani, L., 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.
  2. Kanellakopoulos, I., Kokotovic, P.V., & Morse, A.S. (1991). Systematic design of adaptive controllers for feedback linearizable systems. IEEE Transactions on Automatic Control, 36(11), 1241–1253.
  3. Marino, R., & Tomei, P. (1993). Global adaptive output-feedback control of nonlinear systems, Part I: Linear parameterization. IEEE Transactions on Automatic Control, 38(1), 17–32.
  4. Marino, R., & Tomei, P. (1993). Global adaptive output-feedback control of nonlinear systems, Part II: Nonlinear parameterization. IEEE Transactions on Automatic Control, 38(1), 33–48.
  5. Krstic, M., Kanellakopoulos, I., & Kokotovic, P.V. (1992). Adaptive nonlinear control without overparametrization. Systems & Control Letters, 19(3), 177–185.
  6. Kojić, A., & Annaswamy, A.M. (2002). Adaptive control of nonlinearly parameterized systems with a triangular structure. Automatica, 38(1), 115–123.
  7. Lin, W., & Qian, C. (2002). Adaptive control of nonlinearly parameterized systems: The smooth feedback case. IEEE Transactions on Automatic Control, 47(8), 1249–1266.
  8. Xie, L.-L., & Guo, L. (1999). Fundamental limitations of discrete-time adaptive nonlinear control. IEEE Transactions on Automatic Control, 44(9), 1777–1782.
  9. Sanner, R.M., & Slotine, J.-J.E. (1992). Gaussian networks for direct adaptive control. IEEE Transactions on Neural Networks, 3(6), 837–863.
  10. Narendra, K.S., & Annaswamy, A.M. (1987). A new adaptive law for robust adaptation without persistent excitation. IEEE Transactions on Automatic Control, 32(2), 134–145.
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.