Chapter 8: Robust Modifications for MRAC
Lesson 4: Trade-Off Between Robustness and Parameter Convergence
This lesson explains why robust modifications that suppress parameter drift can also prevent exact parameter recovery. Starting from the first-order MRAC error model developed in earlier chapters, we derive Lyapunov bounds for the sigma-modification, obtain an explicit leakage-induced equilibrium bias, compare sigma-, e-, and dead-zone modifications, and validate the trade-off in five programming environments.
1. Learning Objectives and Prerequisites
After completing this lesson, students should be able to:
- distinguish tracking-error convergence from parameter convergence;
- explain why bounded disturbances and modeling errors may cause parameter drift in an unmodified adaptive law;
- prove uniform ultimate boundedness for a sigma-modified MRAC error system;
- calculate the steady-state tracking and parameter bias introduced by leakage;
- compare sigma-modification, e-modification, and dead-zone adaptation;
- tune a robust modification from noise bounds and acceptable bias.
The lesson assumes the Lyapunov-based MRAC constructions of Chapters 5–7 and the robust modifications introduced in Lessons 2 and 3 of this chapter. A formal excitation condition for exact parameter convergence is intentionally deferred to Chapter 10.
2. Baseline MRAC Error Model
Consider the scalar tracking-error model
\[ \dot e = -a_m e + b\,\widetilde{\boldsymbol{\theta}}^{\mathsf T} \boldsymbol{\phi}(t) + d(t), \qquad a_m > 0, \]
where \(e=y-y_m\) is the tracking error, \(\boldsymbol{\phi}(t)\) is the measurable regressor, \(b\) has known positive sign, and
\[ \widetilde{\boldsymbol{\theta}} = \widehat{\boldsymbol{\theta}} - \boldsymbol{\theta}^{*} \]
is the parameter estimation error. The constant vector \(\boldsymbol{\theta}^{*}\) denotes ideal controller parameters under the matching assumptions developed in Chapter 2. The signal \(d(t)\) collects bounded disturbances and the part of the unmodeled dynamics represented at the error-model level.
The plain gradient/Lyapunov update law is
\[ \dot{\widehat{\boldsymbol{\theta}}} = -\boldsymbol{\Gamma}\,b\, \boldsymbol{\phi}(t)e, \qquad \boldsymbol{\Gamma} = \boldsymbol{\Gamma}^{\mathsf T} > 0. \]
In the ideal case \(d(t)=0\), use
\[ V = \frac{1}{2}e^2 + \frac{1}{2} \widetilde{\boldsymbol{\theta}}^{\mathsf T} \boldsymbol{\Gamma}^{-1} \widetilde{\boldsymbol{\theta}}. \]
Differentiation and cancellation of the cross term give
\[ \begin{aligned} \dot V &= e\dot e + \widetilde{\boldsymbol{\theta}}^{\mathsf T} \boldsymbol{\Gamma}^{-1} \dot{\widehat{\boldsymbol{\theta}}} \\ &= -a_m e^2 + b e \widetilde{\boldsymbol{\theta}}^{\mathsf T} \boldsymbol{\phi} - b \widetilde{\boldsymbol{\theta}}^{\mathsf T} \boldsymbol{\phi}e \\ &= -a_m e^2 \le 0. \end{aligned} \]
This proves boundedness and supports convergence of the tracking error under the usual regularity assumptions. It does not, by itself, prove \(\widehat{\boldsymbol{\theta}} \longrightarrow \boldsymbol{\theta}^{*}\) . Exact parameter recovery requires additional information in the regressor. Chapter 10 formalizes that requirement.
3. Why Robustness and Parameter Convergence Compete
An adaptive controller uses nonzero error as information. Unfortunately, the error may be caused by two fundamentally different mechanisms:
- a genuine controller-parameter mismatch that adaptation should remove;
- noise, delay, disturbances, or unmodeled dynamics that cannot be removed by changing the nominal parameters.
A plain adaptive law cannot perfectly distinguish these mechanisms. Therefore it may continue moving the parameters in response to residual errors that are not informative about \(\boldsymbol{\theta}^{*}\). Robust modifications deliberately reduce this sensitivity.
flowchart TD
A["Measured tracking error"] --> B{"What generated \nthe error?"}
B --> C["True parameter mismatch"]
B --> D["Noise, delay, disturbance, \nor unmodeled dynamics"]
C --> E["Fast adaptation is useful"]
D --> F["Fast adaptation may create drift \nor high-gain behavior"]
E --> G["Favor convergence and \nlow nominal bias"]
F --> H["Add leakage, error scaling, \nor a dead zone"]
G --> I["Design compromise"]
H --> I
I --> J["Bounded parameters with \nacceptable tracking residual"]
The central design conflict is therefore:
\[ \text{stronger suppression of parameter motion} \Longleftrightarrow \text{greater bias or slower parameter convergence}. \]
In practical robust adaptive control, the goal is often not exact identification. The goal is bounded internal signals and acceptable tracking despite uncertainty.
4. Sigma-Modification and a Lyapunov Ultimate Bound
The sigma-modified law adds constant leakage:
\[ \dot{\widehat{\boldsymbol{\theta}}} = -\boldsymbol{\Gamma}b\boldsymbol{\phi}e - \sigma\boldsymbol{\Gamma} \widehat{\boldsymbol{\theta}}, \qquad \sigma > 0. \]
Assume the ideal parameters are constant and \(|d(t)|\le\bar d\). Using the same Lyapunov function,
\[ \begin{aligned} \dot V &= -a_m e^2 + e d - \sigma \widetilde{\boldsymbol{\theta}}^{\mathsf T} \widehat{\boldsymbol{\theta}} \\ &= -a_m e^2 + e d - \sigma \left\| \widetilde{\boldsymbol{\theta}} \right\|^2 - \sigma \widetilde{\boldsymbol{\theta}}^{\mathsf T} \boldsymbol{\theta}^{*}. \end{aligned} \]
Young's inequality gives
\[ e d \le \frac{a_m}{2}e^2 + \frac{\bar d^2}{2a_m}, \]
\[ -\sigma \widetilde{\boldsymbol{\theta}}^{\mathsf T} \boldsymbol{\theta}^{*} \le \frac{\sigma}{2} \left\| \widetilde{\boldsymbol{\theta}} \right\|^2 + \frac{\sigma}{2} \left\| \boldsymbol{\theta}^{*} \right\|^2. \]
Consequently,
\[ \dot V \le -\frac{a_m}{2}e^2 - \frac{\sigma}{2} \left\| \widetilde{\boldsymbol{\theta}} \right\|^2 + \frac{\bar d^2}{2a_m} + \frac{\sigma}{2} \left\| \boldsymbol{\theta}^{*} \right\|^2. \]
Define
\[ \alpha = \min \left\{ a_m,\, \sigma\lambda_{\min}(\boldsymbol{\Gamma}) \right\}, \qquad C = \frac{\bar d^2}{2a_m} + \frac{\sigma}{2} \left\| \boldsymbol{\theta}^{*} \right\|^2. \]
Since \(\|\widetilde{\boldsymbol{\theta}}\|^2 \ge \lambda_{\min}(\boldsymbol{\Gamma}) \widetilde{\boldsymbol{\theta}}^{\mathsf T} \boldsymbol{\Gamma}^{-1} \widetilde{\boldsymbol{\theta}}\) , we obtain
\[ \dot V \le -\alpha V+C. \]
The comparison lemma yields
\[ V(t) \le \exp(-\alpha t)V(0) + \frac{C}{\alpha} \left( 1-\exp(-\alpha t) \right), \]
\[ \limsup_{t\longrightarrow\infty}V(t) \le \frac{C}{\alpha}. \]
Thus sigma-modification changes an ideal asymptotic statement into a robust ultimate-boundedness statement. The leakage supplies direct damping in parameter space, but the term involving \(\boldsymbol{\theta}^{*}\) prevents the Lyapunov derivative from being negative definite at the ideal parameter vector.
5. Exact Calculation of Leakage-Induced Bias
The trade-off is especially clear for a scalar parameter, constant regressor \(\phi_0\ne0\), and zero disturbance:
\[ \dot e = -a_m e + b\phi_0 \left( \widehat\theta-\theta^{*} \right), \]
\[ \dot{\widehat\theta} = -\gamma b\phi_0 e - \gamma\sigma\widehat\theta. \]
At equilibrium,
\[ 0 = -a_m e_{\mathrm{eq}} + b\phi_0 \left( \widehat\theta_{\mathrm{eq}}-\theta^{*} \right), \]
\[ 0 = -b\phi_0 e_{\mathrm{eq}} - \sigma\widehat\theta_{\mathrm{eq}}. \]
Solving these two equations gives
\[ \boxed{ \widehat\theta_{\mathrm{eq}} = \frac{b^2\phi_0^2} {b^2\phi_0^2+a_m\sigma} \theta^{*} } \]
\[ \boxed{ e_{\mathrm{eq}} = - \frac{\sigma b\phi_0} {b^2\phi_0^2+a_m\sigma} \theta^{*} }. \]
Several important conclusions follow:
- the adaptation gain \(\gamma\) affects transient speed but not this equilibrium;
- any \(\sigma>0\) generally prevents exact convergence to \(\theta^{*}\);
- increasing \(\sigma\) increases parameter damping but also increases the nominal tracking residual;
- stronger regressor magnitude reduces the relative leakage bias.
For positive \(b,\phi_0,\theta^{*}\),
\[ \frac{\partial} {\partial\sigma} \left( \frac{\widehat\theta_{\mathrm{eq}}}{\theta^{*}} \right) = - \frac{ a_m b^2\phi_0^2 }{ \left( b^2\phi_0^2+a_m\sigma \right)^2 } < 0, \]
\[ \frac{\partial |e_{\mathrm{eq}}|} {\partial\sigma} = \frac{ b^3\phi_0^3|\theta^{*}| }{ \left( b^2\phi_0^2+a_m\sigma \right)^2 } > 0. \]
6. E-Modification and Dead-Zone Adaptation
6.1 E-Modification
The e-modification scales leakage by the measured error magnitude:
\[ \dot{\widehat{\boldsymbol{\theta}}} = -\boldsymbol{\Gamma}b\boldsymbol{\phi}e_m - \sigma_e |e_m| \boldsymbol{\Gamma} \widehat{\boldsymbol{\theta}}, \qquad e_m=e+n. \]
When the tracking error is large, strong damping protects the adaptive loop. When the tracking error approaches zero, the leakage approaches zero and the nominal parameter bias can be much smaller than with constant sigma-modification. The cost is that the parameter damping is weakest near the origin, exactly where small persistent disturbances or measurement errors may continue to act.
6.2 Dead-Zone Adaptation
Define the switching factor
\[ D_{\delta}(e_m) = \begin{cases} 0, & |e_m|\le\delta, \\ 1, & |e_m|>\delta. \end{cases} \]
The dead-zone update is
\[ \dot{\widehat{\boldsymbol{\theta}}} = - D_{\delta}(e_m) \boldsymbol{\Gamma}b \boldsymbol{\phi}e_m. \]
Once the error lies inside a region consistent with measurement noise and unavoidable model mismatch, adaptation stops. This prevents noise-driven parameter motion and introduces no continuous pull toward zero. However, the final parameter estimate depends on the point at which adaptation enters the dead zone, so exact parameter convergence is generally lost.
7. Comparative Design Properties
| Adaptive law | Parameter damping | Nominal parameter bias | Noise/disturbance behavior | Typical use |
|---|---|---|---|---|
| Plain gradient law | None | Potentially zero in the ideal informative case | May chase residual errors or drift | Clean models, simulation, identification-oriented studies |
| Sigma-modification | Constant | Persistent leakage bias | Strongest direct suppression of drift | Persistent uncertainty and safety-oriented boundedness |
| E-modification | Error dependent | Usually smaller near good tracking | Damping weakens as error becomes small | Compromise between robustness and nominal accuracy |
| Dead zone | Adaptation disabled inside a band | Frozen-estimate error | Good rejection of known noise floor | Reliable bound on noise/modeling residual is available |
7.1 Tracking Convergence Is Not Identification
A controller can achieve small tracking error while the parameter estimate remains different from the physical or ideal parameter. Closed-loop feedback may compensate for several parameter combinations, disturbances may be absorbed into the estimate, and robust terms may intentionally bias the estimate. Therefore,
\[ e(t)\longrightarrow0 \quad \nRightarrow \quad \widetilde{\boldsymbol{\theta}}(t) \longrightarrow\boldsymbol{0}. \]
7.2 A Practical Tuning Sequence
flowchart TD
A["Start from the nominal MRAC design"] --> B["Estimate sensor-noise and modeling-error floor"]
B --> C{"Is a reliable error \nbound available?"}
C -->|yes| D["Set a dead zone slightly \nabove the bound"]
C -->|no| E{"Does the estimate drift \nunder stress tests?"}
E -->|no| F["Keep the plain law or \nuse very mild modification"]
E -->|yes| G{"Is nominal parameter \nbias critical?"}
G -->|yes| H["Use e-modification \nor small sigma"]
G -->|no| I["Use sigma-modification \nfor stronger damping"]
D --> J["Test ideal and disturbed cases"]
F --> J
H --> J
I --> J
J --> K["Check tracking RMS, parameter range, and control effort"]
K --> L["Retune gamma and robustification separately"]
Adaptation gain and robustification gain should not be treated as the same tuning knob. The gain \(\boldsymbol{\Gamma}\) determines how rapidly error information moves the estimate. The leakage or dead-zone parameter determines how much of that motion is suppressed.
8. Comparative Numerical Experiment
The downloadable implementations simulate
\[ \dot e = -2e + \phi(t) \left( \widehat\theta-1.5 \right) + d(t), \qquad \phi(t) = 1+0.8\sin(0.7t), \]
with \(\gamma=8\), \(\sigma=0.15\), \(\sigma_e=0.8\), and dead-zone radius \(\delta=0.08\). The disturbed case uses
\[ d(t) = 0.12\sin(5t) + 0.06\operatorname{sgn} \left( \sin(0.35t) \right), \]
\[ n(t)=0.02\sin(37t), \qquad e_m=e+n. \]
The following values are produced by the supplied deterministic implementation using a midpoint integration step of \(10^{-3}\,\mathrm{s}\).
| Case | Law | RMS error, final 10 s | Final estimate | Final parameter error |
|---|---|---|---|---|
| Ideal | Plain | \(1.19\times10^{-12}\) | \(1.5000\) | \(0.0000\) |
| Ideal | Sigma | \(0.1581\) | \(1.2513\) | \(-0.2487\) |
| Ideal | E-modification | \(0.0212\) | \(1.4978\) | \(-0.0022\) |
| Ideal | Dead zone | \(0.0251\) | \(1.4579\) | \(-0.0421\) |
| Disturbed | Plain | \(0.0298\) | \(1.4252\) | \(-0.0748\) |
| Disturbed | Sigma | \(0.1663\) | \(1.2198\) | \(-0.2802\) |
| Disturbed | E-modification | \(0.1254\) | \(1.3598\) | \(-0.1402\) |
| Disturbed | Dead zone | \(0.0280\) | \(1.4351\) | \(-0.0649\) |
These numerical values are not a universal ranking. They demonstrate that a robustification coefficient selected for strong damping can produce a visible nominal bias. A fair design comparison must test a range of disturbance levels, adaptation gains, and robustification parameters.
8.1 Software Libraries Used in the Implementations
-
Python:
NumPystores trajectories and computes metrics;Matplotlibplots tracking and parameter histories.scipy.integrate.solve_ivpis a suitable alternative when adaptive dynamics are embedded in larger models. -
C++: the supplied scalar implementation uses only the
C++17 standard library. For vector or MIMO MRAC,
Eigenis commonly used for matrix and vector operations. -
Java: the supplied program uses the Java standard
library.
EJMLor Apache Commons Math can support higher-dimensional matrix computations. - MATLAB/Simulink: base MATLAB performs numerical integration and plotting. Simulink provides block-diagram simulation; Control System Toolbox can be used to construct and analyze the nominal reference model.
-
Wolfram Mathematica:
NDSolveValueintegrates the coupled error and adaptation equations, while symbolic functions can verify equilibrium and Lyapunov calculations.
9. Python Implementation
Chapter8_Lesson4.py
# Chapter8_Lesson4.py
# Trade-off between robustness and parameter convergence in scalar MRAC.
# Requires: numpy, matplotlib
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import csv
import math
import matplotlib.pyplot as plt
import numpy as np
@dataclass(frozen=True)
class Config:
a_m: float = 2.0
b: float = 1.0
gamma: float = 8.0
theta_star: float = 1.5
sigma: float = 0.15
sigma_e: float = 0.80
dead_zone: float = 0.08
dt: float = 0.001
final_time: float = 40.0
def regressor(t: float) -> float:
"""Bounded time-varying regressor used by every controller."""
return 1.0 + 0.8 * math.sin(0.7 * t)
def disturbance(t: float, enabled: bool) -> float:
if not enabled:
return 0.0
square = 1.0 if math.sin(0.35 * t) >= 0.0 else -1.0
return 0.12 * math.sin(5.0 * t) + 0.06 * square
def sensor_noise(t: float, enabled: bool) -> float:
return 0.02 * math.sin(37.0 * t) if enabled else 0.0
def derivatives(
t: float,
error: float,
theta_hat: float,
mode: str,
disturbed: bool,
cfg: Config,
) -> tuple[float, float]:
phi = regressor(t)
measured_error = error + sensor_noise(t, disturbed)
# Baseline gradient term.
gradient = -cfg.gamma * cfg.b * phi * measured_error
if mode == "plain":
leakage = 0.0
elif mode == "sigma":
leakage = cfg.gamma * cfg.sigma * theta_hat
elif mode == "e":
leakage = cfg.gamma * cfg.sigma_e * abs(measured_error) * theta_hat
elif mode == "deadzone":
leakage = 0.0
if abs(measured_error) <= cfg.dead_zone:
gradient = 0.0
else:
raise ValueError(f"Unknown mode: {mode}")
error_dot = (
-cfg.a_m * error
+ cfg.b * phi * (theta_hat - cfg.theta_star)
+ disturbance(t, disturbed)
)
theta_dot = gradient - leakage
return error_dot, theta_dot
def simulate(mode: str, disturbed: bool, cfg: Config) -> dict[str, np.ndarray]:
steps = int(round(cfg.final_time / cfg.dt)) + 1
time = np.linspace(0.0, cfg.final_time, steps)
error = np.zeros(steps)
theta_hat = np.zeros(steps)
error[0] = 1.0
theta_hat[0] = 0.0
# Explicit midpoint (RK2): simple, deterministic, and more accurate than Euler.
for k in range(steps - 1):
t = float(time[k])
e = float(error[k])
th = float(theta_hat[k])
k1_e, k1_th = derivatives(t, e, th, mode, disturbed, cfg)
mid_t = t + 0.5 * cfg.dt
mid_e = e + 0.5 * cfg.dt * k1_e
mid_th = th + 0.5 * cfg.dt * k1_th
k2_e, k2_th = derivatives(mid_t, mid_e, mid_th, mode, disturbed, cfg)
error[k + 1] = e + cfg.dt * k2_e
theta_hat[k + 1] = th + cfg.dt * k2_th
return {"time": time, "error": error, "theta_hat": theta_hat}
def metrics(result: dict[str, np.ndarray], cfg: Config) -> dict[str, float]:
time = result["time"]
error = result["error"]
theta_hat = result["theta_hat"]
tail = time >= (cfg.final_time - 10.0)
return {
"rms_error_last_10s": float(np.sqrt(np.mean(error[tail] ** 2))),
"max_error_last_10s": float(np.max(np.abs(error[tail]))),
"final_theta_hat": float(theta_hat[-1]),
"parameter_error": float(theta_hat[-1] - cfg.theta_star),
}
def save_csv(
path: Path,
results: dict[str, dict[str, np.ndarray]],
) -> None:
modes = list(results)
time = results[modes[0]]["time"]
with path.open("w", newline="", encoding="utf-8") as handle:
writer = csv.writer(handle)
header = ["time"]
for mode in modes:
header.extend([f"{mode}_error", f"{mode}_theta_hat"])
writer.writerow(header)
for k, t in enumerate(time):
row: list[float] = [float(t)]
for mode in modes:
row.extend(
[
float(results[mode]["error"][k]),
float(results[mode]["theta_hat"][k]),
]
)
writer.writerow(row)
def plot_results(
path: Path,
results: dict[str, dict[str, np.ndarray]],
cfg: Config,
title_suffix: str,
) -> None:
fig, axes = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
for mode, result in results.items():
axes[0].plot(result["time"], result["error"], label=mode)
axes[1].plot(result["time"], result["theta_hat"], label=mode)
axes[0].set_ylabel("tracking error e")
axes[0].set_title(f"MRAC robustness/convergence comparison: {title_suffix}")
axes[0].grid(True)
axes[0].legend()
axes[1].axhline(cfg.theta_star, linestyle="--", label="ideal parameter")
axes[1].set_xlabel("time (s)")
axes[1].set_ylabel("parameter estimate")
axes[1].grid(True)
axes[1].legend()
fig.tight_layout()
fig.savefig(path, dpi=180)
plt.close(fig)
def run_case(disturbed: bool, cfg: Config) -> None:
modes = ("plain", "sigma", "e", "deadzone")
results = {mode: simulate(mode, disturbed, cfg) for mode in modes}
label = "disturbed" if disturbed else "ideal"
print(f"\nCase: {label}")
for mode in modes:
print(f"{mode:8s}: {metrics(results[mode], cfg)}")
save_csv(Path(f"Chapter8_Lesson4_{label}.csv"), results)
plot_results(
Path(f"Chapter8_Lesson4_{label}.png"),
results,
cfg,
title_suffix=label,
)
def main() -> None:
cfg = Config()
run_case(disturbed=False, cfg=cfg)
run_case(disturbed=True, cfg=cfg)
if __name__ == "__main__":
main()
10. C++ Implementation
Chapter8_Lesson4.cpp
// Chapter8_Lesson4.cpp
// Trade-off between robustness and parameter convergence in scalar MRAC.
// Build: g++ -std=c++17 -O2 Chapter8_Lesson4.cpp -o Chapter8_Lesson4
#include <cmath>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
struct Config {
double a_m = 2.0;
double b = 1.0;
double gamma = 8.0;
double theta_star = 1.5;
double sigma = 0.15;
double sigma_e = 0.80;
double dead_zone = 0.08;
double dt = 0.001;
double final_time = 40.0;
};
struct Result {
std::vector<double> time;
std::vector<double> error;
std::vector<double> theta_hat;
};
double regressor(double t) {
return 1.0 + 0.8 * std::sin(0.7 * t);
}
double disturbance(double t, bool enabled) {
if (!enabled) {
return 0.0;
}
const double square = std::sin(0.35 * t) >= 0.0 ? 1.0 : -1.0;
return 0.12 * std::sin(5.0 * t) + 0.06 * square;
}
double sensor_noise(double t, bool enabled) {
return enabled ? 0.02 * std::sin(37.0 * t) : 0.0;
}
std::pair<double, double> derivatives(
double t,
double error,
double theta_hat,
const std::string& mode,
bool disturbed,
const Config& cfg
) {
const double phi = regressor(t);
const double measured_error = error + sensor_noise(t, disturbed);
double gradient = -cfg.gamma * cfg.b * phi * measured_error;
double leakage = 0.0;
if (mode == "plain") {
leakage = 0.0;
} else if (mode == "sigma") {
leakage = cfg.gamma * cfg.sigma * theta_hat;
} else if (mode == "e") {
leakage = cfg.gamma * cfg.sigma_e * std::abs(measured_error) * theta_hat;
} else if (mode == "deadzone") {
if (std::abs(measured_error) <= cfg.dead_zone) {
gradient = 0.0;
}
} else {
throw std::invalid_argument("Unknown mode: " + mode);
}
const double error_dot =
-cfg.a_m * error
+ cfg.b * phi * (theta_hat - cfg.theta_star)
+ disturbance(t, disturbed);
const double theta_dot = gradient - leakage;
return {error_dot, theta_dot};
}
Result simulate(const std::string& mode, bool disturbed, const Config& cfg) {
const std::size_t steps =
static_cast<std::size_t>(std::llround(cfg.final_time / cfg.dt)) + 1U;
Result result;
result.time.resize(steps);
result.error.assign(steps, 0.0);
result.theta_hat.assign(steps, 0.0);
result.error[0] = 1.0;
for (std::size_t k = 0; k + 1U < steps; ++k) {
const double t = static_cast<double>(k) * cfg.dt;
result.time[k] = t;
const double e = result.error[k];
const double th = result.theta_hat[k];
const auto k1 = derivatives(t, e, th, mode, disturbed, cfg);
const double mid_t = t + 0.5 * cfg.dt;
const double mid_e = e + 0.5 * cfg.dt * k1.first;
const double mid_th = th + 0.5 * cfg.dt * k1.second;
const auto k2 = derivatives(mid_t, mid_e, mid_th, mode, disturbed, cfg);
result.error[k + 1U] = e + cfg.dt * k2.first;
result.theta_hat[k + 1U] = th + cfg.dt * k2.second;
}
result.time.back() = cfg.final_time;
return result;
}
void print_metrics(const std::string& mode, const Result& result, const Config& cfg) {
double sum_sq = 0.0;
double max_abs = 0.0;
std::size_t count = 0U;
for (std::size_t k = 0; k < result.time.size(); ++k) {
if (result.time[k] >= cfg.final_time - 10.0) {
const double abs_error = std::abs(result.error[k]);
sum_sq += result.error[k] * result.error[k];
max_abs = std::max(max_abs, abs_error);
++count;
}
}
const double rms = std::sqrt(sum_sq / static_cast<double>(count));
const double final_theta = result.theta_hat.back();
std::cout << std::setw(8) << mode
<< " RMS(last 10 s)=" << rms
<< " max|e|=" << max_abs
<< " theta_hat(T)=" << final_theta
<< " parameter error=" << final_theta - cfg.theta_star
<< '\n';
}
void save_csv(
const std::string& filename,
const std::vector<std::string>& modes,
const std::map<std::string, Result>& results
) {
std::ofstream file(filename);
if (!file) {
throw std::runtime_error("Cannot open " + filename);
}
file << "time";
for (const auto& mode : modes) {
file << ',' << mode << "_error," << mode << "_theta_hat";
}
file << '\n';
const auto& time = results.at(modes.front()).time;
file << std::setprecision(12);
for (std::size_t k = 0; k < time.size(); ++k) {
file << time[k];
for (const auto& mode : modes) {
file << ',' << results.at(mode).error[k]
<< ',' << results.at(mode).theta_hat[k];
}
file << '\n';
}
}
void run_case(bool disturbed, const Config& cfg) {
const std::vector<std::string> modes = {
"plain", "sigma", "e", "deadzone"
};
std::map<std::string, Result> results;
for (const auto& mode : modes) {
results.emplace(mode, simulate(mode, disturbed, cfg));
}
const std::string label = disturbed ? "disturbed" : "ideal";
std::cout << "\nCase: " << label << '\n';
for (const auto& mode : modes) {
print_metrics(mode, results.at(mode), cfg);
}
save_csv("Chapter8_Lesson4_" + label + "_cpp.csv", modes, results);
}
int main() {
try {
const Config cfg;
run_case(false, cfg);
run_case(true, cfg);
} catch (const std::exception& ex) {
std::cerr << "Error: " << ex.what() << '\n';
return 1;
}
return 0;
}
11. Java Implementation
Chapter8_Lesson4.java
// Chapter8_Lesson4.java
// Trade-off between robustness and parameter convergence in scalar MRAC.
// Build and run:
// javac Chapter8_Lesson4.java
// java Chapter8_Lesson4
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
public final class Chapter8_Lesson4 {
private Chapter8_Lesson4() {}
private static final class Config {
final double aM = 2.0;
final double b = 1.0;
final double gamma = 8.0;
final double thetaStar = 1.5;
final double sigma = 0.15;
final double sigmaE = 0.80;
final double deadZone = 0.08;
final double dt = 0.001;
final double finalTime = 40.0;
}
private static final class Result {
final double[] time;
final double[] error;
final double[] thetaHat;
Result(int steps) {
time = new double[steps];
error = new double[steps];
thetaHat = new double[steps];
}
}
private static double regressor(double t) {
return 1.0 + 0.8 * Math.sin(0.7 * t);
}
private static double disturbance(double t, boolean enabled) {
if (!enabled) {
return 0.0;
}
double square = Math.sin(0.35 * t) >= 0.0 ? 1.0 : -1.0;
return 0.12 * Math.sin(5.0 * t) + 0.06 * square;
}
private static double sensorNoise(double t, boolean enabled) {
return enabled ? 0.02 * Math.sin(37.0 * t) : 0.0;
}
private static double[] derivatives(
double t,
double error,
double thetaHat,
String mode,
boolean disturbed,
Config cfg
) {
double phi = regressor(t);
double measuredError = error + sensorNoise(t, disturbed);
double gradient = -cfg.gamma * cfg.b * phi * measuredError;
double leakage = 0.0;
switch (mode) {
case "plain":
break;
case "sigma":
leakage = cfg.gamma * cfg.sigma * thetaHat;
break;
case "e":
leakage =
cfg.gamma * cfg.sigmaE * Math.abs(measuredError) * thetaHat;
break;
case "deadzone":
if (Math.abs(measuredError) <= cfg.deadZone) {
gradient = 0.0;
}
break;
default:
throw new IllegalArgumentException("Unknown mode: " + mode);
}
double errorDot =
-cfg.aM * error
+ cfg.b * phi * (thetaHat - cfg.thetaStar)
+ disturbance(t, disturbed);
double thetaDot = gradient - leakage;
return new double[] {errorDot, thetaDot};
}
private static Result simulate(
String mode,
boolean disturbed,
Config cfg
) {
int steps = (int) Math.round(cfg.finalTime / cfg.dt) + 1;
Result result = new Result(steps);
result.error[0] = 1.0;
for (int k = 0; k < steps - 1; ++k) {
double t = k * cfg.dt;
result.time[k] = t;
double e = result.error[k];
double theta = result.thetaHat[k];
double[] k1 = derivatives(
t, e, theta, mode, disturbed, cfg
);
double midT = t + 0.5 * cfg.dt;
double midE = e + 0.5 * cfg.dt * k1[0];
double midTheta = theta + 0.5 * cfg.dt * k1[1];
double[] k2 = derivatives(
midT, midE, midTheta, mode, disturbed, cfg
);
result.error[k + 1] = e + cfg.dt * k2[0];
result.thetaHat[k + 1] = theta + cfg.dt * k2[1];
}
result.time[steps - 1] = cfg.finalTime;
return result;
}
private static void printMetrics(
String mode,
Result result,
Config cfg
) {
double sumSquared = 0.0;
double maxAbsolute = 0.0;
int count = 0;
for (int k = 0; k < result.time.length; ++k) {
if (result.time[k] >= cfg.finalTime - 10.0) {
double absolute = Math.abs(result.error[k]);
sumSquared += result.error[k] * result.error[k];
maxAbsolute = Math.max(maxAbsolute, absolute);
++count;
}
}
double rms = Math.sqrt(sumSquared / count);
double finalTheta = result.thetaHat[result.thetaHat.length - 1];
System.out.printf(
Locale.US,
"%8s RMS(last 10 s)=%.8f max|e|=%.8f"
+ " theta_hat(T)=%.8f parameter error=%.8f%n",
mode,
rms,
maxAbsolute,
finalTheta,
finalTheta - cfg.thetaStar
);
}
private static void saveCsv(
Path path,
String[] modes,
Map<String, Result> results
) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
writer.write("time");
for (String mode : modes) {
writer.write("," + mode + "_error," + mode + "_theta_hat");
}
writer.newLine();
Result first = results.get(modes[0]);
for (int k = 0; k < first.time.length; ++k) {
writer.write(String.format(Locale.US, "%.12f", first.time[k]));
for (String mode : modes) {
Result result = results.get(mode);
writer.write(
String.format(
Locale.US,
",%.12f,%.12f",
result.error[k],
result.thetaHat[k]
)
);
}
writer.newLine();
}
}
}
private static void runCase(
boolean disturbed,
Config cfg
) throws IOException {
String[] modes = {"plain", "sigma", "e", "deadzone"};
Map<String, Result> results = new LinkedHashMap<>();
for (String mode : modes) {
results.put(mode, simulate(mode, disturbed, cfg));
}
String label = disturbed ? "disturbed" : "ideal";
System.out.println("\nCase: " + label);
for (String mode : modes) {
printMetrics(mode, results.get(mode), cfg);
}
saveCsv(
Path.of("Chapter8_Lesson4_" + label + "_java.csv"),
modes,
results
);
}
public static void main(String[] args) {
Config cfg = new Config();
try {
runCase(false, cfg);
runCase(true, cfg);
} catch (IOException ex) {
System.err.println("I/O error: " + ex.getMessage());
System.exit(1);
}
}
}
12. MATLAB Implementation
Chapter8_Lesson4.m
% Chapter8_Lesson4.m
% Trade-off between robustness and parameter convergence in scalar MRAC.
% This script compares plain, sigma-modified, e-modified, and dead-zone laws.
clear; clc; close all;
cfg.a_m = 2.0;
cfg.b = 1.0;
cfg.gamma = 8.0;
cfg.theta_star = 1.5;
cfg.sigma = 0.15;
cfg.sigma_e = 0.80;
cfg.dead_zone = 0.08;
cfg.dt = 0.001;
cfg.final_time = 40.0;
modes = ["plain", "sigma", "e", "deadzone"];
run_case(false, cfg, modes);
run_case(true, cfg, modes);
function run_case(disturbed, cfg, modes)
results = cell(size(modes));
for i = 1:numel(modes)
results{i} = simulate_mrac(modes(i), disturbed, cfg);
end
if disturbed
label = "disturbed";
else
label = "ideal";
end
fprintf('\nCase: %s\n', label);
for i = 1:numel(modes)
print_metrics(modes(i), results{i}, cfg);
end
save_case_csv("Chapter8_Lesson4_" + label + "_matlab.csv", ...
modes, results);
figure('Name', "Chapter08 Lesson4 " + label);
tiledlayout(2, 1);
nexttile;
hold on;
for i = 1:numel(modes)
plot(results{i}.time, results{i}.error, ...
'DisplayName', modes(i));
end
grid on;
ylabel('tracking error e');
title("MRAC robustness/convergence comparison: " + label);
legend('Location', 'best');
nexttile;
hold on;
for i = 1:numel(modes)
plot(results{i}.time, results{i}.theta_hat, ...
'DisplayName', modes(i));
end
yline(cfg.theta_star, '--', 'ideal parameter');
grid on;
xlabel('time (s)');
ylabel('parameter estimate');
legend('Location', 'best');
exportgraphics(gcf, ...
"Chapter8_Lesson4_" + label + "_matlab.png", ...
'Resolution', 180);
end
function result = simulate_mrac(mode, disturbed, cfg)
steps = round(cfg.final_time / cfg.dt) + 1;
time = linspace(0.0, cfg.final_time, steps)';
error = zeros(steps, 1);
theta_hat = zeros(steps, 1);
error(1) = 1.0;
for k = 1:(steps - 1)
t = time(k);
e = error(k);
theta = theta_hat(k);
[k1_e, k1_theta] = derivatives( ...
t, e, theta, mode, disturbed, cfg);
mid_t = t + 0.5 * cfg.dt;
mid_e = e + 0.5 * cfg.dt * k1_e;
mid_theta = theta + 0.5 * cfg.dt * k1_theta;
[k2_e, k2_theta] = derivatives( ...
mid_t, mid_e, mid_theta, mode, disturbed, cfg);
error(k + 1) = e + cfg.dt * k2_e;
theta_hat(k + 1) = theta + cfg.dt * k2_theta;
end
result.time = time;
result.error = error;
result.theta_hat = theta_hat;
end
function [error_dot, theta_dot] = derivatives( ...
t, error, theta_hat, mode, disturbed, cfg)
phi = 1.0 + 0.8 * sin(0.7 * t);
if disturbed
square_wave = 2.0 * (sin(0.35 * t) >= 0.0) - 1.0;
d = 0.12 * sin(5.0 * t) + 0.06 * square_wave;
noise = 0.02 * sin(37.0 * t);
else
d = 0.0;
noise = 0.0;
end
measured_error = error + noise;
gradient = -cfg.gamma * cfg.b * phi * measured_error;
leakage = 0.0;
switch mode
case "plain"
% No robust modification.
case "sigma"
leakage = cfg.gamma * cfg.sigma * theta_hat;
case "e"
leakage = cfg.gamma * cfg.sigma_e ...
* abs(measured_error) * theta_hat;
case "deadzone"
if abs(measured_error) <= cfg.dead_zone
gradient = 0.0;
end
otherwise
error('Unknown mode: %s', mode);
end
error_dot = -cfg.a_m * error ...
+ cfg.b * phi * (theta_hat - cfg.theta_star) + d;
theta_dot = gradient - leakage;
end
function print_metrics(mode, result, cfg)
tail = result.time >= (cfg.final_time - 10.0);
rms_error = sqrt(mean(result.error(tail).^2));
max_error = max(abs(result.error(tail)));
final_theta = result.theta_hat(end);
fprintf(['%8s RMS(last 10 s)=%.8f max|e|=%.8f ' ...
'theta_hat(T)=%.8f parameter error=%.8f\n'], ...
mode, rms_error, max_error, final_theta, ...
final_theta - cfg.theta_star);
end
function save_case_csv(filename, modes, results)
output = table(results{1}.time, 'VariableNames', {'time'});
for i = 1:numel(modes)
output.(modes(i) + "_error") = results{i}.error;
output.(modes(i) + "_theta_hat") = results{i}.theta_hat;
end
writetable(output, filename);
end
13. MATLAB/Simulink Model Builder
The following script creates a continuous-time Simulink model for the constant-regressor sigma-modified equations used in Section 5. It logs the tracking error and parameter estimate and compares the simulated final values with the analytical equilibrium.
Chapter8_Lesson4_Simulink.m
% Chapter8_Lesson4_Simulink.m
% Programmatically builds a Simulink model for sigma-modified scalar MRAC.
% The model uses constant regressor phi = 1 and zero disturbance so that the
% leakage-induced equilibrium bias can be compared directly with the formula
% derived in the lesson.
clear; clc;
model = 'Chapter8_Lesson4_Simulink';
if bdIsLoaded(model)
close_system(model, 0);
end
if isfile(model + ".slx")
delete(model + ".slx");
end
new_system(model);
open_system(model);
% Parameters placed in the base workspace.
assignin('base', 'a_m', 2.0);
assignin('base', 'b', 1.0);
assignin('base', 'gamma', 8.0);
assignin('base', 'sigma', 0.15);
assignin('base', 'theta_star', 1.5);
add_block('simulink/Continuous/Integrator', ...
model + "/Error state e", ...
'InitialCondition', '1.0', ...
'Position', [550 100 580 130]);
add_block('simulink/Continuous/Integrator', ...
model + "/Parameter estimate theta_hat", ...
'InitialCondition', '0.0', ...
'Position', [550 260 580 290]);
add_block('simulink/Math Operations/Gain', ...
model + "/-a_m", ...
'Gain', '-a_m', ...
'Position', [250 85 330 115]);
add_block('simulink/Sources/Constant', ...
model + "/theta_star", ...
'Value', 'theta_star', ...
'Position', [90 185 150 215]);
add_block('simulink/Math Operations/Sum', ...
model + "/theta_tilde", ...
'Inputs', '+-', ...
'Position', [245 180 275 220]);
add_block('simulink/Math Operations/Gain', ...
model + "/b_phi", ...
'Gain', 'b', ...
'Position', [330 180 400 220]);
add_block('simulink/Math Operations/Sum', ...
model + "/error_dot", ...
'Inputs', '++', ...
'Position', [465 105 495 145]);
add_block('simulink/Math Operations/Gain', ...
model + "/-gamma_b", ...
'Gain', '-gamma*b', ...
'Position', [250 285 340 315]);
add_block('simulink/Math Operations/Gain', ...
model + "/-gamma_sigma", ...
'Gain', '-gamma*sigma', ...
'Position', [250 350 350 380]);
add_block('simulink/Math Operations/Sum', ...
model + "/theta_dot", ...
'Inputs', '++', ...
'Position', [465 285 495 325]);
add_block('simulink/Sinks/Scope', ...
model + "/Scope", ...
'NumInputPorts', '2', ...
'Position', [690 155 730 235]);
add_block('simulink/Sinks/To Workspace', ...
model + "/e_out", ...
'VariableName', 'e_out', ...
'SaveFormat', 'Structure With Time', ...
'Position', [665 75 765 105]);
add_block('simulink/Sinks/To Workspace', ...
model + "/theta_out", ...
'VariableName', 'theta_out', ...
'SaveFormat', 'Structure With Time', ...
'Position', [665 275 765 305]);
% Error dynamics: e_dot = -a_m e + b(theta_hat - theta_star).
add_line(model, ...
'Error state e/1', '-a_m/1', ...
'autorouting', 'on');
add_line(model, ...
'-a_m/1', 'error_dot/1', ...
'autorouting', 'on');
add_line(model, ...
'Parameter estimate theta_hat/1', 'theta_tilde/1', ...
'autorouting', 'on');
add_line(model, ...
'theta_star/1', 'theta_tilde/2', ...
'autorouting', 'on');
add_line(model, ...
'theta_tilde/1', 'b_phi/1', ...
'autorouting', 'on');
add_line(model, ...
'b_phi/1', 'error_dot/2', ...
'autorouting', 'on');
add_line(model, ...
'error_dot/1', 'Error state e/1', ...
'autorouting', 'on');
% Adaptation: theta_dot = -gamma*b*e - gamma*sigma*theta_hat.
add_line(model, ...
'Error state e/1', '-gamma_b/1', ...
'autorouting', 'on');
add_line(model, ...
'-gamma_b/1', 'theta_dot/1', ...
'autorouting', 'on');
add_line(model, ...
'Parameter estimate theta_hat/1', '-gamma_sigma/1', ...
'autorouting', 'on');
add_line(model, ...
'-gamma_sigma/1', 'theta_dot/2', ...
'autorouting', 'on');
add_line(model, ...
'theta_dot/1', 'Parameter estimate theta_hat/1', ...
'autorouting', 'on');
% Logging and visualization.
add_line(model, ...
'Error state e/1', 'Scope/1', ...
'autorouting', 'on');
add_line(model, ...
'Parameter estimate theta_hat/1', 'Scope/2', ...
'autorouting', 'on');
add_line(model, ...
'Error state e/1', 'e_out/1', ...
'autorouting', 'on');
add_line(model, ...
'Parameter estimate theta_hat/1', 'theta_out/1', ...
'autorouting', 'on');
set_param(model, ...
'StopTime', '20', ...
'Solver', 'ode45', ...
'MaxStep', '0.01');
save_system(model);
sim(model);
b_value = evalin('base', 'b');
a_value = evalin('base', 'a_m');
sigma_value = evalin('base', 'sigma');
theta_value = evalin('base', 'theta_star');
theta_equilibrium = ...
(b_value^2 / (b_value^2 + a_value * sigma_value)) * theta_value;
error_equilibrium = ...
-(sigma_value * b_value / ...
(b_value^2 + a_value * sigma_value)) * theta_value;
fprintf('Predicted theta equilibrium: %.8f\n', theta_equilibrium);
fprintf('Predicted error equilibrium: %.8f\n', error_equilibrium);
fprintf('Simulated final theta: %.8f\n', theta_out.signals.values(end));
fprintf('Simulated final error: %.8f\n', e_out.signals.values(end));
open_system(model);
14. Wolfram Mathematica Implementation
Chapter8_Lesson4.nb
Notebook[{
Cell["Chapter 8, Lesson 4: Robustness vs. Parameter Convergence", "Title"],
Cell["This notebook compares plain, sigma-modified, e-modified, and dead-zone scalar MRAC laws in ideal and disturbed cases.", "Text"],
Cell[BoxData[
RowBox[{
RowBox[{"ClearAll", "[", "\"Global`*\"", "]"}], ";"}]], "Input"],
Cell[BoxData[
RowBox[{
RowBox[{"cfg", "=",
RowBox[{"<|",
RowBox[{
RowBox[{"\"am\"", "->", "2.0"}], ",",
RowBox[{"\"b\"", "->", "1.0"}], ",",
RowBox[{"\"gamma\"", "->", "8.0"}], ",",
RowBox[{"\"thetaStar\"", "->", "1.5"}], ",",
RowBox[{"\"sigma\"", "->", "0.15"}], ",",
RowBox[{"\"sigmaE\"", "->", "0.80"}], ",",
RowBox[{"\"deadZone\"", "->", "0.08"}], ",",
RowBox[{"\"finalTime\"", "->", "40.0"}]
}], "|>"}]}], ";"}]], "Input"],
Cell[BoxData[
RowBox[{
RowBox[{"phi", "[", "t_", "]"}], ":=",
RowBox[{"1.0", "+",
RowBox[{"0.8", " ", RowBox[{"Sin", "[", RowBox[{"0.7", " ", "t"}], "]"}]}]}]}]], "Input"],
Cell[BoxData[
RowBox[{
RowBox[{"disturbance", "[", RowBox[{"t_", ",", "enabled_"}], "]"}], ":=",
RowBox[{"If", "[",
RowBox[{"enabled", ",",
RowBox[{
RowBox[{"0.12", " ", RowBox[{"Sin", "[", RowBox[{"5.0", " ", "t"}], "]"}]}], "+",
RowBox[{"0.06", " ", RowBox[{"Sign", "[", RowBox[{"Sin", "[", RowBox[{"0.35", " ", "t"}], "]"}], "]"}]}]}], ",",
"0.0"}], "]"}]}]], "Input"],
Cell[BoxData[
RowBox[{
RowBox[{"sensorNoise", "[", RowBox[{"t_", ",", "enabled_"}], "]"}], ":=",
RowBox[{"If", "[",
RowBox[{"enabled", ",",
RowBox[{"0.02", " ", RowBox[{"Sin", "[", RowBox[{"37.0", " ", "t"}], "]"}]}], ",",
"0.0"}], "]"}]}]], "Input"],
Cell[BoxData[
RowBox[{
RowBox[{"simulate", "[", RowBox[{"mode_String", ",", "disturbed_"}], "]"}], ":=",
RowBox[{"Module", "[",
RowBox[{
RowBox[{"{", RowBox[{"am", ",", "b", ",", "gamma", ",", "thetaStar", ",",
"sigma", ",", "sigmaE", ",", "deadZone", ",", "law", ",", "sol"}], "}"}], ",",
RowBox[{
RowBox[{"am", "=", RowBox[{"cfg", "[", "\"am\"", "]"}]}], ";",
RowBox[{"b", "=", RowBox[{"cfg", "[", "\"b\"", "]"}]}], ";",
RowBox[{"gamma", "=", RowBox[{"cfg", "[", "\"gamma\"", "]"}]}], ";",
RowBox[{"thetaStar", "=", RowBox[{"cfg", "[", "\"thetaStar\"", "]"}]}], ";",
RowBox[{"sigma", "=", RowBox[{"cfg", "[", "\"sigma\"", "]"}]}], ";",
RowBox[{"sigmaE", "=", RowBox[{"cfg", "[", "\"sigmaE\"", "]"}]}], ";",
RowBox[{"deadZone", "=", RowBox[{"cfg", "[", "\"deadZone\"", "]"}]}], ";",
RowBox[{
RowBox[{"law", "[", RowBox[{"t_", ",", "ee_", ",", "th_"}], "]"}], ":=",
RowBox[{"Module", "[",
RowBox[{
RowBox[{"{", RowBox[{"em", ",", "gradient", ",", "leakage"}], "}"}], ",",
RowBox[{
RowBox[{"em", "=", RowBox[{"ee", "+", RowBox[{"sensorNoise", "[", RowBox[{"t", ",", "disturbed"}], "]"}]}]}], ";",
RowBox[{"gradient", "=", RowBox[{"-", "gamma", " ", "b", " ", RowBox[{"phi", "[", "t", "]"}], " ", "em"}]}], ";",
RowBox[{"leakage", "=", "0"}], ";",
RowBox[{"Switch", "[",
RowBox[{"mode", ",",
"\"plain\"", ",", "Null", ",",
"\"sigma\"", ",", RowBox[{"leakage", "=", RowBox[{"gamma", " ", "sigma", " ", "th"}]}], ",",
"\"e\"", ",", RowBox[{"leakage", "=", RowBox[{"gamma", " ", "sigmaE", " ", RowBox[{"Abs", "[", "em", "]"}], " ", "th"}]}], ",",
"\"deadzone\"", ",", RowBox[{"If", "[", RowBox[{RowBox[{RowBox[{"Abs", "[", "em", "]"}], "<=", "deadZone"}], ",", RowBox[{"gradient", "=", "0"}]}], "]"}]
}], "]"}], ";",
RowBox[{"gradient", "-", "leakage"}]
}]}], "]"}]}], ";",
RowBox[{"sol", "=", RowBox[{"NDSolveValue", "[",
RowBox[{
RowBox[{"{",
RowBox[{
RowBox[{
RowBox[{"e", "'", "[", "t", "]"}], "==",
RowBox[{
RowBox[{"-", "am", " ", RowBox[{"e", "[", "t", "]"}]}], "+",
RowBox[{"b", " ", RowBox[{"phi", "[", "t", "]"}], " ",
RowBox[{"(", RowBox[{RowBox[{"th", "[", "t", "]"}], "-", "thetaStar"}], ")"}]}], "+",
RowBox[{"disturbance", "[", RowBox[{"t", ",", "disturbed"}], "]"}]
}]}], ",",
RowBox[{
RowBox[{"th", "'", "[", "t", "]"}], "==",
RowBox[{"law", "[", RowBox[{"t", ",", RowBox[{"e", "[", "t", "]"}], ",", RowBox[{"th", "[", "t", "]"}]}], "]"}]}], ",",
RowBox[{RowBox[{"e", "[", "0", "]"}], "==", "1.0"}], ",",
RowBox[{RowBox[{"th", "[", "0", "]"}], "==", "0.0"}]
}], "}"}], ",",
RowBox[{"{", RowBox[{"e", ",", "th"}], "}"}], ",",
RowBox[{"{", RowBox[{"t", ",", "0", ",", RowBox[{"cfg", "[", "\"finalTime\"", "]"}]}], "}"}],
",", RowBox[{"MaxStepFraction", "->", RowBox[{"1", "/", "5000"}]}]
}], "]"}]}], ";",
"sol"
}]}], "]"}]}]], "Input"],
Cell[BoxData[
RowBox[{
RowBox[{"modes", "=", RowBox[{"{", RowBox[{"\"plain\"", ",", "\"sigma\"", ",", "\"e\"", ",", "\"deadzone\""}], "}"}]}], ";",
RowBox[{"ideal", "=", RowBox[{"AssociationMap", "[", RowBox[{RowBox[{RowBox[{"simulate", "[", RowBox[{"#", ",", "False"}], "]"}], "&"}], ",", "modes"}], "]"}]}], ";",
RowBox[{"disturbed", "=", RowBox[{"AssociationMap", "[", RowBox[{RowBox[{RowBox[{"simulate", "[", RowBox[{"#", ",", "True"}], "]"}], "&"}], ",", "modes"}], "]"}]}], ";"}]], "Input"],
Cell[BoxData[
RowBox[{"GraphicsGrid", "[",
RowBox[{"{",
RowBox[{
RowBox[{"{",
RowBox[{
RowBox[{"Plot", "[",
RowBox[{
RowBox[{"Evaluate", "[", RowBox[{"Table", "[", RowBox[{RowBox[{RowBox[{"ideal", "[", "m", "]"}], "[", RowBox[{"[", "1", "]"}], "]"}], "[", "t", "]"}], ",", RowBox[{"{", RowBox[{"m", ",", "modes"}], "}"}]}], "]"}], "]"}], ",",
RowBox[{"{", RowBox[{"t", ",", "0", ",", "40"}], "}"}], ",",
RowBox[{"PlotLegends", "->", "modes"}], ",",
RowBox[{"PlotLabel", "->", "\"Ideal: tracking error\""}]
}], "]"}], ",",
RowBox[{"Plot", "[",
RowBox[{
RowBox[{"Evaluate", "[", RowBox[{"Table", "[", RowBox[{RowBox[{RowBox[{"ideal", "[", "m", "]"}], "[", RowBox[{"[", "2", "]"}], "]"}], "[", "t", "]"}], ",", RowBox[{"{", RowBox[{"m", ",", "modes"}], "}"}]}], "]"}], "]"}], ",",
RowBox[{"{", RowBox[{"t", ",", "0", ",", "40"}], "}"}], ",",
RowBox[{"PlotLegends", "->", "modes"}], ",",
RowBox[{"Epilog", "->", RowBox[{"{", RowBox[{"Dashed", ",", RowBox[{"InfiniteLine", "[", RowBox[{"{", RowBox[{"{", RowBox[{"0", ",", "1.5"}], "}"}], ",", RowBox[{"{", RowBox[{"1", ",", "0"}], "}"}]}], "]"}]}], "}"}]}], ",",
RowBox[{"PlotLabel", "->", "\"Ideal: parameter estimate\""}]
}], "]"}]
}], "}"}], ",",
RowBox[{"{",
RowBox[{
RowBox[{"Plot", "[",
RowBox[{
RowBox[{"Evaluate", "[", RowBox[{"Table", "[", RowBox[{RowBox[{RowBox[{"disturbed", "[", "m", "]"}], "[", RowBox[{"[", "1", "]"}], "]"}], "[", "t", "]"}], ",", RowBox[{"{", RowBox[{"m", ",", "modes"}], "}"}]}], "]"}], "]"}], ",",
RowBox[{"{", RowBox[{"t", ",", "0", ",", "40"}], "}"}], ",",
RowBox[{"PlotLegends", "->", "modes"}], ",",
RowBox[{"PlotLabel", "->", "\"Disturbed: tracking error\""}]
}], "]"}], ",",
RowBox[{"Plot", "[",
RowBox[{
RowBox[{"Evaluate", "[", RowBox[{"Table", "[", RowBox[{RowBox[{RowBox[{"disturbed", "[", "m", "]"}], "[", RowBox[{"[", "2", "]"}], "]"}], "[", "t", "]"}], ",", RowBox[{"{", RowBox[{"m", ",", "modes"}], "}"}]}], "]"}], "]"}], ",",
RowBox[{"{", RowBox[{"t", ",", "0", ",", "40"}], "}"}], ",",
RowBox[{"PlotLegends", "->", "modes"}], ",",
RowBox[{"Epilog", "->", RowBox[{"{", RowBox[{"Dashed", ",", RowBox[{"InfiniteLine", "[", RowBox[{"{", RowBox[{"{", RowBox[{"0", ",", "1.5"}], "}"}], ",", RowBox[{"{", RowBox[{"1", ",", "0"}], "}"}]}], "]"}]}], "}"}]}], ",",
RowBox[{"PlotLabel", "->", "\"Disturbed: parameter estimate\""}]
}], "]"}]
}], "}"}]
}], "}"}], "]"}]], "Input"]
},
WindowSize->{1200, 800},
StyleDefinitions->"Default.nb"
]
15. Problems and Solutions
Problem 1: Ultimate Bound with Sigma-Modification
For
\[ \dot e = -a_m e + b \widetilde{\boldsymbol{\theta}}^{\mathsf T} \boldsymbol{\phi} + d, \qquad |d|\le\bar d, \]
and the sigma-modified law of Section 4, prove that \(\dot V\le-\alpha V+C\) and state an ultimate bound for \(V\).
Solution:
Substitute the error and adaptive dynamics into the derivative of \(V\). The gradient cross terms cancel, leaving
\[ \dot V = -a_m e^2 + ed - \sigma \widetilde{\boldsymbol{\theta}}^{\mathsf T} \widehat{\boldsymbol{\theta}}. \]
Write \(\widehat{\boldsymbol{\theta}} = \widetilde{\boldsymbol{\theta}} + \boldsymbol{\theta}^{*}\) and apply Young's inequality to the disturbance and ideal-parameter cross terms. This gives
\[ \dot V \le -\frac{a_m}{2}e^2 - \frac{\sigma}{2} \left\| \widetilde{\boldsymbol{\theta}} \right\|^2 + C, \]
\[ C = \frac{\bar d^2}{2a_m} + \frac{\sigma}{2} \left\| \boldsymbol{\theta}^{*} \right\|^2. \]
With \(\alpha=\min\{a_m, \sigma\lambda_{\min}(\boldsymbol{\Gamma})\}\) ,
\[ \dot V\le-\alpha V+C, \qquad \limsup_{t\longrightarrow\infty}V(t) \le \frac{C}{\alpha}. \]
Problem 2: Numerical Leakage Bias
Let \(a_m=2,\ b=1,\ \phi_0=1,\ \theta^{*}=1.5,\ \sigma=0.15\) . Calculate the equilibrium parameter estimate and tracking error.
Solution:
\[ \widehat\theta_{\mathrm{eq}} = \frac{1}{1+2(0.15)}(1.5) = \frac{1.5}{1.3} \approx 1.15385, \]
\[ e_{\mathrm{eq}} = - \frac{0.15}{1+2(0.15)}(1.5) \approx -0.17308. \]
The parameter estimate is attenuated toward zero and the tracking error must remain nonzero to balance the leakage term.
Problem 3: Maximum Sigma for a Bias Constraint
In the scalar constant-regressor model, require
\[ \frac{ \widehat\theta_{\mathrm{eq}} }{ \theta^{*} } \ge 0.9. \]
Find an upper bound on \(\sigma\).
Solution:
\[ \frac{b^2\phi_0^2} {b^2\phi_0^2+a_m\sigma} \ge 0.9. \]
Rearranging,
\[ b^2\phi_0^2 \ge 0.9b^2\phi_0^2 + 0.9a_m\sigma, \]
\[ \boxed{ \sigma \le \frac{ b^2\phi_0^2 }{ 9a_m } }. \]
This is only a nominal equilibrium-bias constraint. A larger sigma may still be required to satisfy robustness requirements under disturbances.
Problem 4: Dead-Zone Selection
A measured error contains at most \(0.02\) units of sensor noise. Nominal-model validation shows an unavoidable tracking residual of at most \(0.04\). Select a dead-zone radius with an additional \(0.02\) safety margin.
Solution:
\[ \delta = 0.02+0.04+0.02 = 0.08. \]
Adaptation is disabled for \(|e_m|\le0.08\). A smaller dead zone may react to noise; an unnecessarily large dead zone can freeze adaptation before useful correction is complete.
Problem 5: Why E-Modification Usually Has Less Nominal Bias
Compare the leakage magnitudes in sigma- and e-modification as \(|e_m|\) approaches zero.
Solution:
Sigma-modification applies the parameter-space damping
\[ \sigma \boldsymbol{\Gamma} \widehat{\boldsymbol{\theta}} \]
even when the tracking error is arbitrarily small. E-modification applies
\[ \sigma_e|e_m| \boldsymbol{\Gamma} \widehat{\boldsymbol{\theta}}, \]
which approaches zero with \(|e_m|\). Therefore the e-modified law can approach the plain adaptive law near successful tracking and often produces less nominal bias. The same property means that its robust damping also becomes weaker near the origin.
16. Summary
- Tracking-error convergence and parameter convergence are distinct closed-loop properties.
- Plain adaptation has the best nominal opportunity for exact parameter recovery but can react incorrectly to nonparametric residual errors.
- Sigma-modification supplies constant parameter damping and supports a direct uniform-ultimate-boundedness proof.
- Constant leakage generally creates nonzero parameter and tracking bias; the scalar equilibrium quantifies this effect exactly.
- E-modification reduces leakage near good tracking, whereas a dead zone freezes adaptation inside a known uncertainty band.
- Robustification should be selected from disturbance/noise evidence and an explicit acceptable-bias requirement, not from adaptation speed alone.
17. References
- Peterson, B.B., & Narendra, K.S. (1982). Bounded error adaptive control. IEEE Transactions on Automatic Control, 27(6), 1161–1168. doi:10.1109/TAC.1982.1103112.
- Kreisselmeier, G., & Narendra, K.S. (1982). Stable model reference adaptive control in the presence of bounded disturbances. IEEE Transactions on Automatic Control, 27(6), 1169–1175.
- Ioannou, P.A., & Kokotovic, P.V. (1984). Robust redesign of adaptive control. IEEE Transactions on Automatic Control, 29(3), 202–211. doi:10.1109/TAC.1984.1103490.
- Ioannou, P.A., & Kokotovic, P.V. (1984). Instability analysis and improvement of robustness of adaptive control. Automatica, 20(5), 583–594. doi:10.1016/0005-1098(84)90009-8.
- 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. doi:10.1109/TAC.1985.1104070.
- Narendra, K.S., & Annaswamy, A.M. (1986). Robust adaptive control in the presence of bounded disturbances. IEEE Transactions on Automatic Control, 31(4), 306–315. doi:10.1109/TAC.1986.1104259.
- 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. doi:10.1109/TAC.1987.1104543.
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.