Chapter 21: Composite Adaptive Control
Lesson 2: Combining Tracking Error and Prediction Error for Adaptation
This lesson develops the central mechanism of composite adaptive control: one parameter estimate is updated from two physically distinct residuals. The tracking error reports failure of the controlled state to follow the reference model, while the prediction error reports disagreement between measured plant motion and a parameterized state predictor. Their Lyapunov-consistent combination supplies more information to the adaptive law, commonly improving transient tracking and parameter convergence.
1. Learning Objectives and Prerequisites
After completing this lesson, the student should be able to:
- derive tracking-error and prediction-error dynamics for a matched uncertain plant;
- construct a composite parameter update law with correct signs and dimensions;
- show cancellation of state-parameter cross terms in a Lyapunov derivative;
- distinguish tracking convergence from parameter convergence;
- explain the roles of adaptation gain, predictor bandwidth, prediction weight, and excitation;
- implement and compare tracking-only and composite adaptive laws numerically.
The lesson uses concepts already developed in earlier chapters: linear reference models, matching conditions, Lyapunov equations, gradient adaptation, normalization, projection, Barbalat's lemma, and persistent excitation. Dynamic regressor extension and mixing is intentionally deferred to Lesson 4 of this chapter.
2. Why Two Error Signals?
In a conventional direct adaptive controller, parameter updates are generated by a tracking residual. This residual is indispensable because the control objective is tracking. Nevertheless, it is an indirect measurement of parameter mismatch: a parameter error influences the plant, the plant influences the state, and only then does the state error enter the adaptive law.
A predictor creates a second route. It uses the same parameter estimate to predict plant motion. The resulting prediction error is therefore more directly tied to model mismatch. Composite adaptation does not replace the tracking channel; it combines the control-oriented and identification-oriented channels in one update law.
flowchart TD
R["Command r"] --> RM["Reference model"]
R --> C["Adaptive controller"]
RM --> TE["Tracking error e = x - xm"]
C --> P["Uncertain plant"]
P --> X["Measured state x"]
X --> TE
X --> PR["State predictor"]
C --> PR
PR --> PE["Prediction error eps = xhat - x"]
X --> PE
TE --> U["Composite parameter update"]
PE --> U
U --> C
U --> PR
The word composite refers to the composition of information sources. It does not mean that two unrelated adaptive laws may be added arbitrarily. Their signs, weights, and regressors must be compatible with the closed-loop error dynamics and the stability proof.
3. Matched Uncertain Plant and Reference Model
Consider the single-input linearly parameterized plant
\[ \dot{\mathbf{x}} = \mathbf{A}\mathbf{x} + \mathbf{B}\left(u + \boldsymbol{\theta}^{*T}\boldsymbol{\phi}(\mathbf{x},t)\right), \qquad \mathbf{x}\in\mathbb{R}^{n},\quad \boldsymbol{\theta}^{*}\in\mathbb{R}^{p}. \]
The unknown constant vector \(\boldsymbol{\theta}^{*}\) multiplies the known regressor \(\boldsymbol{\phi}(\mathbf{x},t)\). The uncertainty is matched because it acts through the same input vector \(\mathbf{B}\) as the control signal. The desired dynamics are
\[ \dot{\mathbf{x}}_m = \mathbf{A}_m\mathbf{x}_m + \mathbf{B}_m r, \]
where \(\mathbf{A}_m\) is Hurwitz. Suppose the nominal matching conditions admit constant gains \(\mathbf{K}_x\) and \(k_r\) satisfying
\[ \mathbf{A}+\mathbf{B}\mathbf{K}_x^T=\mathbf{A}_m, \qquad \mathbf{B}k_r=\mathbf{B}_m. \]
Use the certainty-equivalent controller
\[ u=\mathbf{K}_x^T\mathbf{x}+k_r r -\hat{\boldsymbol{\theta}}^T\boldsymbol{\phi}(\mathbf{x},t). \]
Define tracking and parameter errors by
\[ \mathbf{e}=\mathbf{x}-\mathbf{x}_m, \qquad \tilde{\boldsymbol{\theta}} =\hat{\boldsymbol{\theta}}-\boldsymbol{\theta}^{*}. \]
Substitution gives the tracking-error model
\[ \dot{\mathbf{e}}=\mathbf{A}_m\mathbf{e} -\mathbf{B}\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi}. \]
Thus the tracking channel contains parameter information only through the scalar product \(\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi}\) and its subsequent effect on \(\mathbf{e}\).
4. State Predictor and Prediction-Error Dynamics
Introduce the stable state predictor
\[ \dot{\hat{\mathbf{x}}} =\mathbf{A}\mathbf{x} +\mathbf{B}\left(u+\hat{\boldsymbol{\theta}}^T\boldsymbol{\phi}\right) +\mathbf{H}(\hat{\mathbf{x}}-\mathbf{x}), \]
where \(\mathbf{H}\) is Hurwitz. The measured state is used in the known nominal part, while the uncertain term is reconstructed with the current parameter estimate. Define
\[ \boldsymbol{\varepsilon}=\hat{\mathbf{x}}-\mathbf{x}. \]
Subtracting the plant dynamics from the predictor dynamics yields
\[ \dot{\boldsymbol{\varepsilon}} =\mathbf{H}\boldsymbol{\varepsilon} +\mathbf{B}\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi}. \]
Notice the sign difference between the parameter term in the tracking and prediction models. That difference determines the signs in the composite update law. Reversing the definition of \(\boldsymbol{\varepsilon}\) is valid, but the adaptive-law sign must then be reversed consistently.
The predictor is not an observer for an unmeasured state in this lesson; the plant state is assumed available. Its purpose is to transform model mismatch into an additional measurable residual.
5. Derivation of the Composite Update Law
Choose symmetric positive-definite matrices satisfying
\[ \mathbf{A}_m^T\mathbf{P}+\mathbf{P}\mathbf{A}_m=-\mathbf{Q}, \qquad \mathbf{H}^T\mathbf{S}+\mathbf{S}\mathbf{H}=-\mathbf{R}, \]
with \(\mathbf{Q}\succ\mathbf{0}\) and \(\mathbf{R}\succ\mathbf{0}\). Let \(\boldsymbol{\Gamma}\succ\mathbf{0}\) be the adaptation-gain matrix and let \(\eta>0\) weight the prediction channel. The composite adaptive law is
\[ \boxed{ \dot{\hat{\boldsymbol{\theta}}} =\boldsymbol{\Gamma}\boldsymbol{\phi} \left(\mathbf{B}^T\mathbf{P}\mathbf{e} -\eta\mathbf{B}^T\mathbf{S}\boldsymbol{\varepsilon}\right)}. \]
The two components are
\[ \dot{\hat{\boldsymbol{\theta}}}_{\mathrm{tr}} =\boldsymbol{\Gamma}\boldsymbol{\phi}\mathbf{B}^T\mathbf{P}\mathbf{e}, \qquad \dot{\hat{\boldsymbol{\theta}}}_{\mathrm{pr}} =-\eta\boldsymbol{\Gamma}\boldsymbol{\phi} \mathbf{B}^T\mathbf{S}\boldsymbol{\varepsilon}. \]
Setting \(\eta=0\) recovers tracking-error-only adaptation. Increasing \(\eta\) does not simply make the controller faster: it raises sensitivity to predictor mismatch and measurement noise.
flowchart TD
A["Measure x and command r"] --> B["Update reference model xm"]
A --> C["Compute control u"]
C --> D["Update plant state x"]
C --> E["Update predictor state xhat"]
B --> F["Compute tracking error e"]
D --> F
D --> G["Compute prediction error eps"]
E --> G
F --> H["Tracking adaptation term"]
G --> I["Prediction adaptation term"]
H --> J["Sum terms and update thetaHat"]
I --> J
J --> C
J --> E
6. Compact Lyapunov Verification
A complete treatment of composite Lyapunov construction follows in Lesson 3. Here we verify why the two update terms must appear in precisely the stated combination. Consider
\[ V=\mathbf{e}^T\mathbf{P}\mathbf{e} +\eta\boldsymbol{\varepsilon}^T\mathbf{S}\boldsymbol{\varepsilon} +\tilde{\boldsymbol{\theta}}^T \boldsymbol{\Gamma}^{-1}\tilde{\boldsymbol{\theta}}. \]
Because the true parameter is constant,
\[ \dot{\tilde{\boldsymbol{\theta}}} =\dot{\hat{\boldsymbol{\theta}}}. \]
Differentiating the tracking-energy term gives
\[ \frac{d}{dt}(\mathbf{e}^T\mathbf{P}\mathbf{e}) =-\mathbf{e}^T\mathbf{Q}\mathbf{e} -2\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi} \mathbf{B}^T\mathbf{P}\mathbf{e}. \]
Differentiating the weighted prediction-energy term gives
\[ \eta\frac{d}{dt} (\boldsymbol{\varepsilon}^T\mathbf{S}\boldsymbol{\varepsilon}) =-\eta\boldsymbol{\varepsilon}^T\mathbf{R}\boldsymbol{\varepsilon} +2\eta\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi} \mathbf{B}^T\mathbf{S}\boldsymbol{\varepsilon}. \]
The parameter-energy derivative is
\[ \frac{d}{dt} (\tilde{\boldsymbol{\theta}}^T\boldsymbol{\Gamma}^{-1} \tilde{\boldsymbol{\theta}}) =2\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi} \left(\mathbf{B}^T\mathbf{P}\mathbf{e} -\eta\mathbf{B}^T\mathbf{S}\boldsymbol{\varepsilon}\right). \]
Adding the three expressions cancels both cross terms:
\[ \boxed{\dot V =-\mathbf{e}^T\mathbf{Q}\mathbf{e} -\eta\boldsymbol{\varepsilon}^T\mathbf{R}\boldsymbol{\varepsilon} \le 0}. \]
Therefore \(V(t)\le V(0)\), and \(\mathbf{e}\), \(\boldsymbol{\varepsilon}\), and \(\tilde{\boldsymbol{\theta}}\) are bounded. Under the usual bounded-signal conditions needed for Barbalat's lemma,
\[ \mathbf{e}(t)→\mathbf{0}, \qquad \boldsymbol{\varepsilon}(t)→\mathbf{0}. \]
This result alone does not force \(\tilde{\boldsymbol{\theta}}(t)→\mathbf{0}\). Parameter convergence requires sufficient excitation or an equivalent information condition.
7. Information Interpretation and Parameter Convergence
The tracking term modifies parameters in directions that reduce the control error. The prediction term modifies them in directions that reduce mismatch between the predicted and measured plant dynamics. Both are multiplied by the regressor, so adaptation can only move along parameter directions revealed by available data.
A standard continuous-time persistent-excitation condition is: there exist constants \(T>0\) and \(\alpha>0\) such that, for every \(t\ge0\),
\[ \int_t^{t+T} \boldsymbol{\phi}(\tau)\boldsymbol{\phi}^T(\tau)\,d\tau \succeq \alpha\mathbf{I}. \]
Under suitable regularity assumptions, this condition prevents a nonzero parameter-error direction from remaining invisible indefinitely. Composite adaptation often improves convergence speed because the prediction residual exposes parameter mismatch more directly, but classical instantaneous composite laws do not automatically remove the need for excitation.
Without sufficient excitation, the controller may achieve excellent tracking while the estimate converges to a constant different from the true parameter. Such a value can be behaviorally equivalent along the realized trajectory. This distinction is fundamental:
\[ \text{tracking convergence}\;\not\Rightarrow\; \text{parameter convergence}. \]
8. Tuning, Filtering, and Robust Implementation
8.1 Adaptation gain
Larger eigenvalues of \(\boldsymbol{\Gamma}\) accelerate parameter motion but amplify residual noise and discretization effects. Adaptation gain should be selected together with sampling rate, actuator bandwidth, and regressor magnitude.
8.2 Predictor dynamics
Faster predictor poles make the prediction error respond rapidly to model mismatch. Excessive predictor bandwidth also passes more measurement noise into the adaptive law. The matrix \(\mathbf{H}\) must remain Hurwitz, and its bandwidth should generally remain below poorly modeled high-frequency plant dynamics.
8.3 Prediction weight
The scalar \(\eta\) adjusts the relative influence of the prediction channel. A practical design begins with a small positive value, verifies stable noise-free behavior, and then increases it while monitoring parameter-rate peaks and control effort.
8.4 Normalized composite update
If the regressor magnitude varies substantially, a normalized law from Chapter 9 can be used:
\[ \dot{\hat{\boldsymbol{\theta}}} =\boldsymbol{\Gamma} \frac{\boldsymbol{\phi}} {1+\boldsymbol{\phi}^T\boldsymbol{\phi}} \left(\mathbf{B}^T\mathbf{P}\mathbf{e} -\eta\mathbf{B}^T\mathbf{S}\boldsymbol{\varepsilon}\right). \]
8.5 Projection and leakage
Projection may enforce physically meaningful parameter bounds. Leakage or a dead zone may be added when disturbances and unmodeled dynamics produce persistent residuals. These modifications improve robustness but generally replace exact parameter convergence by convergence to a neighborhood.
9. Scalar Worked Example
Consider
\[ \dot x=-a x+u+\theta^*\phi(x), \qquad \phi(x)=\tanh(x)+0.25x, \]
with reference model and controller
\[ \dot x_m=-a_m x_m+b_m r, \qquad u=(a-a_m)x+b_m r-\hat\theta\phi(x). \]
The predictor is
\[ \dot{\hat x}=-a x+u+\hat\theta\phi(x) -\ell_p(\hat x-x). \]
With \(e=x-x_m\), \(\varepsilon=\hat x-x\), and \(\tilde\theta=\hat\theta-\theta^*\),
\[ \dot e=-a_m e-\tilde\theta\phi, \qquad \dot\varepsilon=-\ell_p\varepsilon+\tilde\theta\phi. \]
For unit Lyapunov penalties, choose
\[ p=\frac{1}{2a_m},\qquad s=\frac{1}{2\ell_p}, \]
and use
\[ \dot{\hat\theta}=\gamma\phi(pe-\eta s\varepsilon). \]
The scalar Lyapunov function
\[ V=pe^2+\eta s\varepsilon^2+\frac{1}{\gamma}\tilde\theta^2 \]
satisfies
\[ \dot V=-e^2-\eta\varepsilon^2. \]
The supplied programs use \(a=0.8\), \(a_m=1.6\), \(\ell_p=2.5\), \(\gamma=6\), \(\eta=2\), and \(\theta^*=1.2\). A fourth-order Runge-Kutta method with a 1 ms step compares the composite law against the same controller with \(\eta=0\).
| Metric | Tracking-only | Composite |
|---|---|---|
| Full-run tracking RMSE | 0.200693 | 0.152296 |
| Second-half tracking RMSE | 0.005283 | 0.000595 |
| Maximum absolute tracking error | 0.949744 | 0.872044 |
| Final absolute parameter error | 0.004090 | 0.000033 |
These values are numerical results for this example, not universal performance guarantees. Their purpose is to show how a prediction channel can accelerate use of the same regressor information.
10. Python Implementation
Chapter21_Lesson2.py
"""Chapter 21, Lesson 2: composite adaptive control simulation.
Compares a tracking-error-only adaptive law with a composite law that also
uses state-prediction error. The scalar plant is
x_dot = -a*x + u + theta_star*phi(x)
and the controller is chosen so that ideal cancellation produces the reference
model x_m_dot = -a_m*x_m + b_m*r.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
import numpy as np
@dataclass(frozen=True)
class Config:
dt: float = 0.001
final_time: float = 25.0
a: float = 0.8
a_m: float = 1.6
b_m: float = 1.0
theta_star: float = 1.2
predictor_gain: float = 2.5
adaptation_gain: float = 6.0
prediction_weight: float = 2.0
def command(t: float) -> float:
"""Bounded multi-sine command that supplies sustained excitation."""
return 0.8 * np.sin(0.55 * t) + 0.35 * np.sin(1.65 * t)
def regressor(x: float) -> float:
"""Known nonlinear basis multiplying the unknown constant parameter."""
return np.tanh(x) + 0.25 * x
def dynamics(t: float, z: np.ndarray, cfg: Config, composite: bool) -> np.ndarray:
x, x_m, x_hat, theta_hat = z
phi = regressor(x)
r = command(t)
# Matching control: if theta_hat == theta_star, x follows the reference model.
u = (cfg.a - cfg.a_m) * x + cfg.b_m * r - theta_hat * phi
x_dot = -cfg.a * x + u + cfg.theta_star * phi
x_m_dot = -cfg.a_m * x_m + cfg.b_m * r
x_hat_dot = (
-cfg.a * x
+ u
+ theta_hat * phi
- cfg.predictor_gain * (x_hat - x)
)
tracking_error = x - x_m
prediction_error = x_hat - x
p = 1.0 / (2.0 * cfg.a_m)
s = 1.0 / (2.0 * cfg.predictor_gain)
eta = cfg.prediction_weight if composite else 0.0
theta_hat_dot = cfg.adaptation_gain * phi * (
p * tracking_error - eta * s * prediction_error
)
return np.array([x_dot, x_m_dot, x_hat_dot, theta_hat_dot], dtype=float)
def rk4_step(
rhs: Callable[[float, np.ndarray], np.ndarray],
t: float,
z: np.ndarray,
dt: float,
) -> np.ndarray:
k1 = rhs(t, z)
k2 = rhs(t + 0.5 * dt, z + 0.5 * dt * k1)
k3 = rhs(t + 0.5 * dt, z + 0.5 * dt * k2)
k4 = rhs(t + dt, z + dt * k3)
return z + (dt / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)
def simulate(cfg: Config, composite: bool) -> dict[str, np.ndarray]:
steps = int(round(cfg.final_time / cfg.dt)) + 1
time = np.linspace(0.0, cfg.final_time, steps)
state = np.zeros((steps, 4), dtype=float)
state[0] = np.array([0.8, 0.0, 0.0, -1.0])
rhs = lambda t, z: dynamics(t, z, cfg, composite)
for k in range(steps - 1):
state[k + 1] = rk4_step(rhs, time[k], state[k], cfg.dt)
x, x_m, x_hat, theta_hat = state.T
return {
"time": time,
"x": x,
"x_m": x_m,
"x_hat": x_hat,
"theta_hat": theta_hat,
"tracking_error": x - x_m,
"prediction_error": x_hat - x,
}
def metrics(data: dict[str, np.ndarray], cfg: Config) -> dict[str, float]:
e = data["tracking_error"]
half = len(e) // 2
return {
"tracking_rmse": float(np.sqrt(np.mean(e**2))),
"late_tracking_rmse": float(np.sqrt(np.mean(e[half:] ** 2))),
"max_abs_tracking_error": float(np.max(np.abs(e))),
"final_parameter_error": float(abs(data["theta_hat"][-1] - cfg.theta_star)),
}
def save_csv(path: Path, data: dict[str, np.ndarray]) -> None:
matrix = np.column_stack(
[
data["time"],
data["x"],
data["x_m"],
data["x_hat"],
data["tracking_error"],
data["prediction_error"],
data["theta_hat"],
]
)
np.savetxt(
path,
matrix,
delimiter=",",
header="time,x,x_m,x_hat,tracking_error,prediction_error,theta_hat",
comments="",
)
def main() -> None:
cfg = Config()
tracking_only = simulate(cfg, composite=False)
composite = simulate(cfg, composite=True)
save_csv(Path("Chapter21_Lesson2_tracking_only.csv"), tracking_only)
save_csv(Path("Chapter21_Lesson2_composite.csv"), composite)
for label, data in [("tracking-only", tracking_only), ("composite", composite)]:
print(f"\n{label}")
for name, value in metrics(data, cfg).items():
print(f" {name}: {value:.8f}")
try:
import matplotlib.pyplot as plt
except ImportError:
print("\nmatplotlib is not installed; CSV files were still generated.")
return
plt.figure()
plt.plot(composite["time"], composite["x_m"], label="reference")
plt.plot(composite["time"], tracking_only["x"], label="tracking-only")
plt.plot(composite["time"], composite["x"], label="composite")
plt.xlabel("Time (s)")
plt.ylabel("State")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.savefig("Chapter21_Lesson2_tracking.png", dpi=180)
plt.figure()
plt.plot(tracking_only["time"], tracking_only["theta_hat"], label="tracking-only")
plt.plot(composite["time"], composite["theta_hat"], label="composite")
plt.axhline(cfg.theta_star, linestyle="--", label="true parameter")
plt.xlabel("Time (s)")
plt.ylabel("Parameter estimate")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.savefig("Chapter21_Lesson2_parameter.png", dpi=180)
print("\nSaved CSV data and PNG figures in the current directory.")
if __name__ == "__main__":
main()
Required library: numpy. The optional plots use
matplotlib. The implementation writes CSV files even when
plotting is unavailable.
11. C++17 Implementation
Chapter21_Lesson2.cpp
// Chapter 21, Lesson 2: composite adaptive control simulation.
// Compile: g++ -std=c++17 -O2 Chapter21_Lesson2.cpp -o Chapter21_Lesson2
#include <algorithm>
#include <array>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
struct Config {
double dt = 0.001;
double final_time = 25.0;
double a = 0.8;
double a_m = 1.6;
double b_m = 1.0;
double theta_star = 1.2;
double predictor_gain = 2.5;
double adaptation_gain = 6.0;
double prediction_weight = 2.0;
};
using State = std::array<double, 4>; // x, x_m, x_hat, theta_hat
struct Sample {
double time{};
State state{};
};
double command(double t) {
return 0.8 * std::sin(0.55 * t) + 0.35 * std::sin(1.65 * t);
}
double regressor(double x) {
return std::tanh(x) + 0.25 * x;
}
State add(const State& a, const State& b, double scale = 1.0) {
State result{};
for (std::size_t i = 0; i < result.size(); ++i) {
result[i] = a[i] + scale * b[i];
}
return result;
}
State dynamics(double t, const State& z, const Config& cfg, bool composite) {
const double x = z[0];
const double x_m = z[1];
const double x_hat = z[2];
const double theta_hat = z[3];
const double phi = regressor(x);
const double r = command(t);
const double u = (cfg.a - cfg.a_m) * x + cfg.b_m * r - theta_hat * phi;
const double x_dot = -cfg.a * x + u + cfg.theta_star * phi;
const double x_m_dot = -cfg.a_m * x_m + cfg.b_m * r;
const double x_hat_dot = -cfg.a * x + u + theta_hat * phi
- cfg.predictor_gain * (x_hat - x);
const double tracking_error = x - x_m;
const double prediction_error = x_hat - x;
const double p = 1.0 / (2.0 * cfg.a_m);
const double s = 1.0 / (2.0 * cfg.predictor_gain);
const double eta = composite ? cfg.prediction_weight : 0.0;
const double theta_hat_dot = cfg.adaptation_gain * phi
* (p * tracking_error - eta * s * prediction_error);
return {x_dot, x_m_dot, x_hat_dot, theta_hat_dot};
}
State rk4_step(double t, const State& z, const Config& cfg, bool composite) {
const State k1 = dynamics(t, z, cfg, composite);
const State k2 = dynamics(t + 0.5 * cfg.dt, add(z, k1, 0.5 * cfg.dt), cfg, composite);
const State k3 = dynamics(t + 0.5 * cfg.dt, add(z, k2, 0.5 * cfg.dt), cfg, composite);
const State k4 = dynamics(t + cfg.dt, add(z, k3, cfg.dt), cfg, composite);
State next{};
for (std::size_t i = 0; i < next.size(); ++i) {
next[i] = z[i] + (cfg.dt / 6.0) * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]);
}
return next;
}
std::vector<Sample> simulate(const Config& cfg, bool composite) {
const std::size_t steps = static_cast<std::size_t>(std::llround(cfg.final_time / cfg.dt)) + 1U;
std::vector<Sample> data;
data.reserve(steps);
State z{0.8, 0.0, 0.0, -1.0};
for (std::size_t k = 0; k < steps; ++k) {
const double t = static_cast<double>(k) * cfg.dt;
data.push_back({t, z});
if (k + 1U < steps) {
z = rk4_step(t, z, cfg, composite);
}
}
return data;
}
void save_csv(const std::string& filename, const std::vector<Sample>& data) {
std::ofstream out(filename);
if (!out) {
throw std::runtime_error("Could not open " + filename);
}
out << "time,x,x_m,x_hat,tracking_error,prediction_error,theta_hat\n";
out << std::setprecision(12);
for (const auto& sample : data) {
const auto& z = sample.state;
out << sample.time << ',' << z[0] << ',' << z[1] << ',' << z[2] << ','
<< (z[0] - z[1]) << ',' << (z[2] - z[0]) << ',' << z[3] << '\n';
}
}
void print_metrics(const std::string& label, const std::vector<Sample>& data, const Config& cfg) {
double squared_sum = 0.0;
double late_squared_sum = 0.0;
double max_abs_error = 0.0;
const std::size_t half = data.size() / 2U;
for (std::size_t i = 0; i < data.size(); ++i) {
const double e = data[i].state[0] - data[i].state[1];
squared_sum += e * e;
if (i >= half) {
late_squared_sum += e * e;
}
max_abs_error = std::max(max_abs_error, std::abs(e));
}
const double rmse = std::sqrt(squared_sum / static_cast<double>(data.size()));
const double late_rmse = std::sqrt(
late_squared_sum / static_cast<double>(data.size() - half)
);
const double parameter_error = std::abs(data.back().state[3] - cfg.theta_star);
std::cout << '\n' << label << '\n'
<< " tracking_rmse: " << rmse << '\n'
<< " late_tracking_rmse: " << late_rmse << '\n'
<< " max_abs_tracking_error: " << max_abs_error << '\n'
<< " final_parameter_error: " << parameter_error << '\n';
}
int main() {
try {
const Config cfg{};
const auto tracking_only = simulate(cfg, false);
const auto composite = simulate(cfg, true);
save_csv("Chapter21_Lesson2_tracking_only_cpp.csv", tracking_only);
save_csv("Chapter21_Lesson2_composite_cpp.csv", composite);
print_metrics("tracking-only", tracking_only, cfg);
print_metrics("composite", composite, cfg);
std::cout << "\nSaved CSV data in the current directory.\n";
return 0;
} catch (const std::exception& ex) {
std::cerr << "Error: " << ex.what() << '\n';
return 1;
}
}
This implementation uses only the ISO C++ standard library and writes the two simulation histories as CSV files.
12. Java Implementation
Chapter21_Lesson2.java
// Chapter 21, Lesson 2: composite adaptive control simulation.
// Compile: javac Chapter21_Lesson2.java
// Run: java Chapter21_Lesson2
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public final class Chapter21_Lesson2 {
private static final class Config {
final double dt = 0.001;
final double finalTime = 25.0;
final double a = 0.8;
final double am = 1.6;
final double bm = 1.0;
final double thetaStar = 1.2;
final double predictorGain = 2.5;
final double adaptationGain = 6.0;
final double predictionWeight = 2.0;
}
private static final class Sample {
final double time;
final double[] state;
Sample(double time, double[] state) {
this.time = time;
this.state = state.clone();
}
}
private static double command(double t) {
return 0.8 * Math.sin(0.55 * t) + 0.35 * Math.sin(1.65 * t);
}
private static double regressor(double x) {
return Math.tanh(x) + 0.25 * x;
}
private static double[] dynamics(double t, double[] z, Config cfg, boolean composite) {
double x = z[0];
double xm = z[1];
double xHat = z[2];
double thetaHat = z[3];
double phi = regressor(x);
double r = command(t);
double u = (cfg.a - cfg.am) * x + cfg.bm * r - thetaHat * phi;
double xDot = -cfg.a * x + u + cfg.thetaStar * phi;
double xmDot = -cfg.am * xm + cfg.bm * r;
double xHatDot = -cfg.a * x + u + thetaHat * phi
- cfg.predictorGain * (xHat - x);
double trackingError = x - xm;
double predictionError = xHat - x;
double p = 1.0 / (2.0 * cfg.am);
double s = 1.0 / (2.0 * cfg.predictorGain);
double eta = composite ? cfg.predictionWeight : 0.0;
double thetaHatDot = cfg.adaptationGain * phi
* (p * trackingError - eta * s * predictionError);
return new double[] {xDot, xmDot, xHatDot, thetaHatDot};
}
private static double[] add(double[] a, double[] b, double scale) {
double[] result = new double[a.length];
for (int i = 0; i < a.length; i++) {
result[i] = a[i] + scale * b[i];
}
return result;
}
private static double[] rk4Step(double t, double[] z, Config cfg, boolean composite) {
double[] k1 = dynamics(t, z, cfg, composite);
double[] k2 = dynamics(t + 0.5 * cfg.dt, add(z, k1, 0.5 * cfg.dt), cfg, composite);
double[] k3 = dynamics(t + 0.5 * cfg.dt, add(z, k2, 0.5 * cfg.dt), cfg, composite);
double[] k4 = dynamics(t + cfg.dt, add(z, k3, cfg.dt), cfg, composite);
double[] next = new double[z.length];
for (int i = 0; i < z.length; i++) {
next[i] = z[i] + (cfg.dt / 6.0)
* (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]);
}
return next;
}
private static List<Sample> simulate(Config cfg, boolean composite) {
int steps = (int) Math.round(cfg.finalTime / cfg.dt) + 1;
List<Sample> data = new ArrayList<>(steps);
double[] state = new double[] {0.8, 0.0, 0.0, -1.0};
for (int k = 0; k < steps; k++) {
double t = k * cfg.dt;
data.add(new Sample(t, state));
if (k + 1 < steps) {
state = rk4Step(t, state, cfg, composite);
}
}
return data;
}
private static void saveCsv(Path path, List<Sample> data) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
writer.write("time,x,x_m,x_hat,tracking_error,prediction_error,theta_hat\n");
for (Sample sample : data) {
double[] z = sample.state;
writer.write(String.format(
Locale.US,
"%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f%n",
sample.time,
z[0], z[1], z[2],
z[0] - z[1],
z[2] - z[0],
z[3]
));
}
}
}
private static void printMetrics(String label, List<Sample> data, Config cfg) {
double squaredSum = 0.0;
double lateSquaredSum = 0.0;
double maxAbsError = 0.0;
int half = data.size() / 2;
for (int i = 0; i < data.size(); i++) {
double[] z = data.get(i).state;
double e = z[0] - z[1];
squaredSum += e * e;
if (i >= half) {
lateSquaredSum += e * e;
}
maxAbsError = Math.max(maxAbsError, Math.abs(e));
}
double rmse = Math.sqrt(squaredSum / data.size());
double lateRmse = Math.sqrt(lateSquaredSum / (data.size() - half));
double parameterError = Math.abs(data.get(data.size() - 1).state[3] - cfg.thetaStar);
System.out.printf(Locale.US, "%n%s%n", label);
System.out.printf(Locale.US, " tracking_rmse: %.8f%n", rmse);
System.out.printf(Locale.US, " late_tracking_rmse: %.8f%n", lateRmse);
System.out.printf(Locale.US, " max_abs_tracking_error: %.8f%n", maxAbsError);
System.out.printf(Locale.US, " final_parameter_error: %.8f%n", parameterError);
}
public static void main(String[] args) {
Config cfg = new Config();
try {
List<Sample> trackingOnly = simulate(cfg, false);
List<Sample> composite = simulate(cfg, true);
saveCsv(Path.of("Chapter21_Lesson2_tracking_only_java.csv"), trackingOnly);
saveCsv(Path.of("Chapter21_Lesson2_composite_java.csv"), composite);
printMetrics("tracking-only", trackingOnly, cfg);
printMetrics("composite", composite, cfg);
System.out.println("\nSaved CSV data in the current directory.");
} catch (IOException ex) {
System.err.println("I/O error: " + ex.getMessage());
System.exit(1);
}
}
}
The Java version requires no external dependency and uses
java.nio.file for CSV output.
13. MATLAB Implementation
Chapter21_Lesson2.m
% Chapter 21, Lesson 2: composite adaptive control simulation.
% Compares tracking-error-only adaptation against composite adaptation.
clear; clc; close all;
cfg.dt = 0.001;
cfg.finalTime = 25.0;
cfg.a = 0.8;
cfg.am = 1.6;
cfg.bm = 1.0;
cfg.thetaStar = 1.2;
cfg.predictorGain = 2.5;
cfg.adaptationGain = 6.0;
cfg.predictionWeight = 2.0;
trackingOnly = simulateCompositeAdaptive(cfg, false);
composite = simulateCompositeAdaptive(cfg, true);
writetable(struct2table(trackingOnly), 'Chapter21_Lesson2_tracking_only_matlab.csv');
writetable(struct2table(composite), 'Chapter21_Lesson2_composite_matlab.csv');
printMetrics('tracking-only', trackingOnly, cfg);
printMetrics('composite', composite, cfg);
figure;
plot(composite.time, composite.xm, 'LineWidth', 1.3); hold on;
plot(trackingOnly.time, trackingOnly.x, 'LineWidth', 1.0);
plot(composite.time, composite.x, 'LineWidth', 1.0);
grid on;
xlabel('Time (s)'); ylabel('State');
legend('reference', 'tracking-only', 'composite', 'Location', 'best');
title('Tracking comparison');
exportgraphics(gcf, 'Chapter21_Lesson2_tracking_matlab.png', 'Resolution', 180);
figure;
plot(trackingOnly.time, trackingOnly.thetaHat, 'LineWidth', 1.0); hold on;
plot(composite.time, composite.thetaHat, 'LineWidth', 1.0);
yline(cfg.thetaStar, '--', 'true parameter');
grid on;
xlabel('Time (s)'); ylabel('Parameter estimate');
legend('tracking-only', 'composite', 'Location', 'best');
title('Parameter convergence comparison');
exportgraphics(gcf, 'Chapter21_Lesson2_parameter_matlab.png', 'Resolution', 180);
function data = simulateCompositeAdaptive(cfg, useComposite)
steps = round(cfg.finalTime / cfg.dt) + 1;
time = linspace(0.0, cfg.finalTime, steps).';
state = zeros(steps, 4);
state(1, :) = [0.8, 0.0, 0.0, -1.0];
for k = 1:(steps - 1)
t = time(k);
z = state(k, :).';
k1 = rhs(t, z, cfg, useComposite);
k2 = rhs(t + 0.5 * cfg.dt, z + 0.5 * cfg.dt * k1, cfg, useComposite);
k3 = rhs(t + 0.5 * cfg.dt, z + 0.5 * cfg.dt * k2, cfg, useComposite);
k4 = rhs(t + cfg.dt, z + cfg.dt * k3, cfg, useComposite);
state(k + 1, :) = (z + (cfg.dt / 6.0) * (k1 + 2*k2 + 2*k3 + k4)).';
end
data.time = time;
data.x = state(:, 1);
data.xm = state(:, 2);
data.xHat = state(:, 3);
data.trackingError = data.x - data.xm;
data.predictionError = data.xHat - data.x;
data.thetaHat = state(:, 4);
end
function dz = rhs(t, z, cfg, useComposite)
x = z(1);
xm = z(2);
xHat = z(3);
thetaHat = z(4);
phi = tanh(x) + 0.25 * x;
r = 0.8 * sin(0.55 * t) + 0.35 * sin(1.65 * t);
u = (cfg.a - cfg.am) * x + cfg.bm * r - thetaHat * phi;
xDot = -cfg.a * x + u + cfg.thetaStar * phi;
xmDot = -cfg.am * xm + cfg.bm * r;
xHatDot = -cfg.a * x + u + thetaHat * phi ...
- cfg.predictorGain * (xHat - x);
e = x - xm;
epsilon = xHat - x;
p = 1.0 / (2.0 * cfg.am);
s = 1.0 / (2.0 * cfg.predictorGain);
eta = double(useComposite) * cfg.predictionWeight;
thetaHatDot = cfg.adaptationGain * phi * (p * e - eta * s * epsilon);
dz = [xDot; xmDot; xHatDot; thetaHatDot];
end
function printMetrics(label, data, cfg)
halfIndex = floor(numel(data.trackingError) / 2) + 1;
trackingRmse = sqrt(mean(data.trackingError.^2));
lateTrackingRmse = sqrt(mean(data.trackingError(halfIndex:end).^2));
maxAbsTrackingError = max(abs(data.trackingError));
finalParameterError = abs(data.thetaHat(end) - cfg.thetaStar);
fprintf('\n%s\n', label);
fprintf(' tracking_rmse: %.8f\n', trackingRmse);
fprintf(' late_tracking_rmse: %.8f\n', lateTrackingRmse);
fprintf(' max_abs_tracking_error: %.8f\n', maxAbsTrackingError);
fprintf(' final_parameter_error: %.8f\n', finalParameterError);
end
The script uses local functions, writetable, and
exportgraphics. No specialized control toolbox is required.
The same equations can later be implemented in Simulink with Integrator,
Sum, Gain, Product, and MATLAB Function blocks.
14. Wolfram Mathematica Implementation
Chapter21_Lesson2.nb
Notebook[{
Cell["Chapter 21, Lesson 2: Combining Tracking Error and Prediction Error for Adaptation", "Title"],
Cell["This notebook compares tracking-error-only adaptation with a Lyapunov-consistent composite adaptive law.", "Text"],
Cell[BoxData[
RowBox[{"ClearAll", "[", "\"Global`*\"", "]"}]], "Input"],
Cell[BoxData[
RowBox[{
RowBox[{"dt", "=", "0.001"}], ";",
RowBox[{"finalTime", "=", "25.0"}], ";",
RowBox[{"a", "=", "0.8"}], ";",
RowBox[{"am", "=", "1.6"}], ";",
RowBox[{"bm", "=", "1.0"}], ";",
RowBox[{"thetaStar", "=", "1.2"}], ";",
RowBox[{"predictorGain", "=", "2.5"}], ";",
RowBox[{"adaptationGain", "=", "6.0"}], ";",
RowBox[{"predictionWeight", "=", "2.0"}], ";"
}]], "Input"],
Cell[BoxData[
RowBox[{
RowBox[{"command", "[", "t_", "]", ":=", RowBox[{"0.8", " ", RowBox[{"Sin", "[", RowBox[{"0.55", " ", "t"}], "]"}], "+", "0.35", " ", RowBox[{"Sin", "[", RowBox[{"1.65", " ", "t"}], "]"}]}]}], ";",
RowBox[{"regressor", "[", "x_", "]", ":=", RowBox[{RowBox[{"Tanh", "[", "x", "]"}], "+", RowBox[{"0.25", " ", "x"}]}]}], ";"
}]], "Input"],
Cell[BoxData[
RowBox[{
RowBox[{"simulate", "[", "useComposite_", "]", ":=", RowBox[{"Module", "[",
RowBox[{
RowBox[{"{", RowBox[{"eta", ",", "p", ",", "s", ",", "solution", ",", "grid"}], "}"}], ",",
RowBox[{
RowBox[{"eta", "=", RowBox[{"If", "[", RowBox[{"useComposite", ",", "predictionWeight", ",", "0.0"}], "]"}]}], ";",
RowBox[{"p", "=", RowBox[{"1.0", "/", RowBox[{"(", RowBox[{"2.0", " ", "am"}], ")"}]}]}], ";",
RowBox[{"s", "=", RowBox[{"1.0", "/", RowBox[{"(", RowBox[{"2.0", " ", "predictorGain"}], ")"}]}]}], ";",
RowBox[{"solution", "=", RowBox[{"NDSolveValue", "[",
RowBox[{
RowBox[{"{",
RowBox[{
RowBox[{RowBox[{"x", "'"}], "[", "t", "]", "==", RowBox[{RowBox[{"-", "a"}], " ", RowBox[{"x", "[", "t", "]"}], "+", RowBox[{"u", "[", "t", "]"}], "+", RowBox[{"thetaStar", " ", RowBox[{"regressor", "[", RowBox[{"x", "[", "t", "]"}], "]"}]}]}], ",",
RowBox[{RowBox[{"xm", "'"}], "[", "t", "]", "==", RowBox[{RowBox[{"-", "am"}], " ", RowBox[{"xm", "[", "t", "]"}], "+", RowBox[{"bm", " ", RowBox[{"command", "[", "t", "]"}]}]}], ",",
RowBox[{RowBox[{"xhat", "'"}], "[", "t", "]", "==", RowBox[{RowBox[{"-", "a"}], " ", RowBox[{"x", "[", "t", "]"}], "+", RowBox[{"u", "[", "t", "]"}], "+", RowBox[{RowBox[{"thetaHat", "[", "t", "]"}], " ", RowBox[{"regressor", "[", RowBox[{"x", "[", "t", "]"}], "]"}]}], "-", RowBox[{"predictorGain", " ", RowBox[{"(", RowBox[{RowBox[{"xhat", "[", "t", "]"}], "-", RowBox[{"x", "[", "t", "]"}]}], ")"}]}]}], ",",
RowBox[{RowBox[{"thetaHat", "'"}], "[", "t", "]", "==", RowBox[{"adaptationGain", " ", RowBox[{"regressor", "[", RowBox[{"x", "[", "t", "]"}], "]"}], " ", RowBox[{"(", RowBox[{RowBox[{"p", " ", RowBox[{"(", RowBox[{RowBox[{"x", "[", "t", "]"}], "-", RowBox[{"xm", "[", "t", "]"}]}], ")"}]}], "-", RowBox[{"eta", " ", "s", " ", RowBox[{"(", RowBox[{RowBox[{"xhat", "[", "t", "]"}], "-", RowBox[{"x", "[", "t", "]"}]}], ")"}]}]}], ")"}]}]}], ",",
RowBox[{"u", "[", "t", "]", "==", RowBox[{RowBox[{"(", RowBox[{"a", "-", "am"}], ")"}], " ", RowBox[{"x", "[", "t", "]"}], "+", RowBox[{"bm", " ", RowBox[{"command", "[", "t", "]"}]}], "-", RowBox[{RowBox[{"thetaHat", "[", "t", "]"}], " ", RowBox[{"regressor", "[", RowBox[{"x", "[", "t", "]"}], "]"}]}]}], ",",
RowBox[{RowBox[{"x", "[", "0", "]"}], "==", "0.8"}], ",",
RowBox[{RowBox[{"xm", "[", "0", "]"}], "==", "0.0"}], ",",
RowBox[{RowBox[{"xhat", "[", "0", "]"}], "==", "0.0"}], ",",
RowBox[{RowBox[{"thetaHat", "[", "0", "]"}], "==", RowBox[{"-", "1.0"}]}]
}], "}"}], ",",
RowBox[{"{", RowBox[{"x", ",", "xm", ",", "xhat", ",", "thetaHat"}], "}"}], ",",
RowBox[{"{", RowBox[{"t", ",", "0", ",", "finalTime"}], "}"}], ",",
RowBox[{"Method", "->", RowBox[{"{", RowBox[{"\"TimeIntegration\"", "->", RowBox[{"{", RowBox[{"\"ExplicitRungeKutta\"", ",", RowBox[{"\"DifferenceOrder\"", "->", "4"}]}], "}"}]}], "}"}]}], ",",
RowBox[{"MaxStepSize", "->", "dt"}]
}], "]"}]}], ";",
RowBox[{"grid", "=", RowBox[{"Range", "[", RowBox[{"0.0", ",", "finalTime", ",", "0.01"}], "]"}]}], ";",
RowBox[{"Association", "[", RowBox[{
RowBox[{"\"Time\"", "->", "grid"}], ",",
RowBox[{"\"X\"", "->", RowBox[{RowBox[{"solution", "[", RowBox[{"[", "1", "]"}], "]"}], "/@", "grid"}]}], ",",
RowBox[{"\"Xm\"", "->", RowBox[{RowBox[{"solution", "[", RowBox[{"[", "2", "]"}], "]"}], "/@", "grid"}]}], ",",
RowBox[{"\"Xhat\"", "->", RowBox[{RowBox[{"solution", "[", RowBox[{"[", "3", "]"}], "]"}], "/@", "grid"}]}], ",",
RowBox[{"\"ThetaHat\"", "->", RowBox[{RowBox[{"solution", "[", RowBox[{"[", "4", "]"}], "]"}], "/@", "grid"}]}]
}], "]"}]
}]
}], "]"}]}], ";"
}]], "Input"],
Cell[BoxData[
RowBox[{
RowBox[{"trackingOnly", "=", RowBox[{"simulate", "[", "False", "]"}]}], ";",
RowBox[{"composite", "=", RowBox[{"simulate", "[", "True", "]"}]}], ";"
}]], "Input"],
Cell[BoxData[
RowBox[{"ListLinePlot", "[",
RowBox[{
RowBox[{"{",
RowBox[{
RowBox[{"Transpose", "[", RowBox[{"{", RowBox[{RowBox[{"composite", "[", "\"Time\"", "]"}], ",", RowBox[{"composite", "[", "\"Xm\"", "]"}]}], "}"}], "]"}], ",",
RowBox[{"Transpose", "[", RowBox[{"{", RowBox[{RowBox[{"trackingOnly", "[", "\"Time\"", "]"}], ",", RowBox[{"trackingOnly", "[", "\"X\"", "]"}]}], "}"}], "]"}], ",",
RowBox[{"Transpose", "[", RowBox[{"{", RowBox[{RowBox[{"composite", "[", "\"Time\"", "]"}], ",", RowBox[{"composite", "[", "\"X\"", "]"}]}], "}"}], "]"}]
}], "}"}], ",",
RowBox[{"PlotLegends", "->", RowBox[{"{", RowBox[{"\"reference\"", ",", "\"tracking-only\"", ",", "\"composite\""}], "}"}]}], ",",
RowBox[{"Frame", "->", "True"}], ",",
RowBox[{"FrameLabel", "->", RowBox[{"{", RowBox[{"\"Time (s)\"", ",", "\"State\""}], "}"}]}]
}], "]"}]], "Input"],
Cell[BoxData[
RowBox[{"ListLinePlot", "[",
RowBox[{
RowBox[{"{",
RowBox[{
RowBox[{"Transpose", "[", RowBox[{"{", RowBox[{RowBox[{"trackingOnly", "[", "\"Time\"", "]"}], ",", RowBox[{"trackingOnly", "[", "\"ThetaHat\"", "]"}]}], "}"}], "]"}], ",",
RowBox[{"Transpose", "[", RowBox[{"{", RowBox[{RowBox[{"composite", "[", "\"Time\"", "]"}], ",", RowBox[{"composite", "[", "\"ThetaHat\"", "]"}]}], "}"}], "]"}]
}], "}"}], ",",
RowBox[{"PlotLegends", "->", RowBox[{"{", RowBox[{"\"tracking-only\"", ",", "\"composite\""}], "}"}]}], ",",
RowBox[{"Frame", "->", "True"}], ",",
RowBox[{"FrameLabel", "->", RowBox[{"{", RowBox[{"\"Time (s)\"", ",", "\"Parameter estimate\""}], "}"}]}], ",",
RowBox[{"Epilog", "->", RowBox[{"{", RowBox[{"Dashed", ",", RowBox[{"Line", "[", RowBox[{"{", RowBox[{RowBox[{"{", RowBox[{"0", ",", "thetaStar"}], "}"}], ",", RowBox[{"{", RowBox[{"finalTime", ",", "thetaStar"}], "}"}]}], "}"}], "]"}]}], "}"}]}]
}], "]"}]], "Input"]
}, WindowTitle -> "Chapter21_Lesson2", StyleDefinitions -> "Default.nb"]
The notebook uses NDSolveValue with an explicit
fourth-order Runge-Kutta time integrator and generates tracking and
parameter-estimate plots.
15. Simulink Realization
A direct Simulink realization can be organized into five subsystems:
- Reference model: integrate \(\dot x_m=-a_mx_m+b_mr\).
- Plant: integrate \(\dot x=-ax+u+\theta^*\phi(x)\).
- Predictor: integrate the predictor state equation.
- Error construction: compute \(e=x-x_m\) and \(\varepsilon=\hat x-x\).
- Adaptive law: compute \(\gamma\phi(pe-\eta s\varepsilon)\) and integrate it to obtain \(\hat\theta\).
To compare algorithms without duplicating the full model, use a Variant Subsystem or a Manual Switch that selects \(\eta=0\) or \(\eta>0\). Log the state, tracking error, prediction error, estimate, and control input. A fixed-step solver should use a step substantially smaller than the fastest predictor time constant.
16. Problems and Solutions
Problem 1 — Error-model derivation. For the vector plant, reference model, controller, and predictor of Sections 3 and 4, derive the tracking and prediction error equations without skipping algebraic steps.
Solution. Substitute the controller into the plant:
\[ \dot{\mathbf{x}} =(\mathbf{A}+\mathbf{B}\mathbf{K}_x^T)\mathbf{x} +\mathbf{B}k_r r -\mathbf{B}\hat{\boldsymbol{\theta}}^T\boldsymbol{\phi} +\mathbf{B}\boldsymbol{\theta}^{*T}\boldsymbol{\phi}. \]
Use the matching conditions and the parameter-error definition:
\[ \dot{\mathbf{x}} =\mathbf{A}_m\mathbf{x}+\mathbf{B}_m r -\mathbf{B}\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi}. \]
Subtract the reference model:
\[ \dot{\mathbf{e}} =\mathbf{A}_m(\mathbf{x}-\mathbf{x}_m) -\mathbf{B}\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi} =\mathbf{A}_m\mathbf{e} -\mathbf{B}\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi}. \]
For the predictor error, subtract the plant equation from the predictor:
\[ \dot{\boldsymbol{\varepsilon}} =\mathbf{H}(\hat{\mathbf{x}}-\mathbf{x}) +\mathbf{B}(\hat{\boldsymbol{\theta}}-\boldsymbol{\theta}^{*})^T \boldsymbol{\phi} =\mathbf{H}\boldsymbol{\varepsilon} +\mathbf{B}\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi}. \]
Problem 2 — Cross-term cancellation. Starting from the Lyapunov function in Section 6, show explicitly that the proposed adaptive law is the unique gradient-form combination of the displayed tracking and prediction residuals that cancels both cross terms.
Solution. Write a candidate update as
\[ \dot{\hat{\boldsymbol{\theta}}} =\boldsymbol{\Gamma}\boldsymbol{\phi} (c_1\mathbf{B}^T\mathbf{P}\mathbf{e} +c_2\mathbf{B}^T\mathbf{S}\boldsymbol{\varepsilon}). \]
The three parameter cross-term coefficients in \(\dot V\) are
\[ 2(c_1-1)\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi} \mathbf{B}^T\mathbf{P}\mathbf{e} +2(c_2+\eta)\tilde{\boldsymbol{\theta}}^T\boldsymbol{\phi} \mathbf{B}^T\mathbf{S}\boldsymbol{\varepsilon}. \]
Cancellation for arbitrary errors and parameter errors requires \(c_1=1\) and \(c_2=-\eta\). Hence the stated law is forced within this gradient structure.
Problem 3 — Scalar gain calculation. For \(a_m=2\), \(\ell_p=4\), \(\gamma=10\), and \(\eta=3\), compute \(p\), \(s\), and the scalar adaptive law.
Solution.
\[ p=\frac{1}{2a_m}=\frac{1}{4}, \qquad s=\frac{1}{2\ell_p}=\frac{1}{8}. \]
Therefore
\[ \dot{\hat\theta} =10\phi\left(\frac{1}{4}e -3\frac{1}{8}\varepsilon\right) =2.5\phi e-3.75\phi\varepsilon. \]
The numerical coefficients cannot be compared in isolation because the magnitudes and bandwidths of \(e\) and \(\varepsilon\) may differ.
Problem 4 — Tracking without identification. Suppose the scalar regressor satisfies \(\phi(x(t))=0\) after a finite time. Can the Lyapunov result still imply tracking convergence? Can it imply convergence of \(\hat\theta\) to \(\theta^*\)?
Solution. When the regressor becomes zero, the parameter update stops. The stable homogeneous tracking and predictor dynamics become
\[ \dot e=-a_m e,\qquad \dot\varepsilon=-\ell_p\varepsilon, \]
so both residuals converge to zero. However, the frozen parameter estimate need not equal the true parameter. The data no longer contain information about the unknown coefficient, so parameter convergence cannot be inferred.
Problem 5 — Noisy prediction error. Let the measured state be \(x_y=x+n\), where \(n\) is bounded high-frequency noise. Explain why increasing predictor bandwidth and prediction weight simultaneously can be harmful, and propose two modifications already covered in earlier chapters.
Solution. A fast predictor reacts strongly to rapid changes in \(x_y\). The prediction residual then contains a larger high-frequency component. Multiplication by a large \(\eta\) injects that component into the parameter-rate command, which may create chattering-like parameter motion and high control effort. Two suitable modifications are:
- reduce predictor bandwidth or low-pass filter measured signals before forming the residual;
- use normalization and projection so large regressors do not generate excessive update rates and estimates remain inside known bounds.
A dead zone or leakage may also be used, but it changes the asymptotic conclusion and must be included in a revised stability analysis.
17. Summary
Composite adaptive control updates one parameter vector from tracking and prediction residuals. For matched linearly parameterized uncertainty, a stable state predictor produces an error model whose parameter cross term has the opposite sign from the tracking-error cross term. A composite update law derived from a joint Lyapunov function cancels both terms and yields negative-semidefinite energy decay in the two residuals. The added information can improve transients and parameter learning, but true parameter convergence still depends on excitation or a related data-richness condition. Predictor bandwidth, adaptation gain, prediction weight, normalization, and robustness modifications must be tuned as one coupled design.
18. References
- Slotine, J.-J.E., & Li, W. (1989). Composite adaptive control of robot manipulators. Automatica, 25(4), 509–519. doi:10.1016/0005-1098(89)90094-0.
- Duarte-Mermoud, M.A., & Narendra, K.S. (1989). Combined direct and indirect approach to adaptive control. IEEE Transactions on Automatic Control, 34(10), 1071–1075.
- Narendra, K.S., & Annaswamy, A.M. (1987). Persistent excitation in adaptive systems. International Journal of Control, 45(1), 127–160. doi:10.1080/00207178708933715.
- Patre, P.M., MacKunis, W., Johnson, M., & Dixon, W.E. (2010). Composite adaptive control for Euler–Lagrange systems with additive disturbances. Automatica, 46(1), 140–147. doi:10.1016/j.automatica.2009.10.017.
- Chowdhary, G.V., & Johnson, E.N. (2011). Theory and flight-test validation of a concurrent-learning adaptive controller. Journal of Guidance, Control, and Dynamics, 34(2), 592–607. doi:10.2514/1.46866.
- Pan, Y., Zhang, J., & Yu, H. (2016). Model reference composite learning control without persistency of excitation. IET Control Theory & Applications, 10(16), 1963–1971. doi:10.1049/iet-cta.2016.0032.
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.