Chapter 11: Indirect Adaptive Control and Self-Tuning Regulators (STR)
Lesson 5: Links to but Distinction from Offline System Identification Course
System identification and indirect adaptive control use many of the same mathematical objects—parametric models, regressors, prediction errors, and least-squares estimators—but they solve different engineering problems. This lesson establishes the precise connection and the equally important distinction: offline system identification seeks a model from a designed data record, whereas an indirect adaptive controller estimates only what is needed, while operating, to update a feedback law safely and effectively.
1. Learning Objectives and Placement in the Course
After completing this lesson, students should be able to:
- explain why an STR contains an identifier but is not merely an offline identification algorithm connected to a controller;
- distinguish batch estimation, recursive estimation, open-loop experiments, and closed-loop adaptation;
- derive the batch least-squares estimate and its estimation-error covariance;
- derive the recursive least-squares update as a sequential matrix-inversion update;
- show how parameter-estimation error enters the certainty-equivalent tracking-error dynamics;
- explain the regulation–excitation conflict and why good tracking does not necessarily imply parameter convergence;
- compare prediction-oriented and control-oriented notions of model quality.
Chapters 1–10 established adaptive-control motivation, Lyapunov tools, MRAC, robust modifications, normalization, projection, and persistent excitation. Lessons 1–4 of this chapter introduced indirect adaptation, STR architecture, online estimation, and the comparison with MRAC. The present lesson draws the boundary between that material and a full system-identification course. Detailed RLS design, forgetting-factor selection, covariance conditioning, and convergence analysis begin in Chapter 12.
2. A Shared Mathematical Core
Both offline identification and indirect adaptive control may begin with a linearly parameterized discrete-time model:
\[ y(k+1)=\boldsymbol{\phi}^{T}(k)\boldsymbol{\theta}^{\star}+v(k+1), \]
where \( y(k) \) is the measured output, \( \boldsymbol{\phi}(k) \) is a regressor constructed from measured inputs and outputs, \( \boldsymbol{\theta}^{\star} \) is the unknown parameter vector, and \( v(k) \) represents noise or unmodeled effects. The one-step prediction based on an estimate is
\[ \widehat{y}(k+1\mid k) =\boldsymbol{\phi}^{T}(k)\widehat{\boldsymbol{\theta} }(k), \qquad \varepsilon(k+1) =y(k+1)-\widehat{y}(k+1\mid k). \]
The same prediction error can therefore appear in a system-identification algorithm and in an STR. The decisive difference is what surrounds the estimator, what data it receives, what objective is optimized, and what consequences follow from estimation error.
flowchart LR
subgraph OFF["Offline identification"]
E1["Design experiment"] --> D1["Collect fixed data record"]
D1 --> M1["Select model structure and estimate parameters"]
M1 --> V1["Validate prediction and simulation behavior"]
V1 --> C1["Design controller after identification"]
end
subgraph STR["Indirect adaptive control / STR"]
R["Reference r(k)"] --> CTRL["Controller computed from theta_hat(k)"]
CTRL --> P["Plant"]
P --> Y["Measured output y(k)"]
Y --> ID["Online identifier"]
CTRL --> ID
ID --> UP["Update theta_hat(k)"]
UP --> CTRL
end
3. Offline System Identification: The Model Is the Primary Product
In a conventional offline identification study, an experiment produces a finite data record \( \mathcal{Z}_{N}=\{u(k),y(k)\}_{k=1}^{N} \). The record is stored, inspected, divided into estimation and validation subsets, and processed after the experiment. For a linear regression,
\[ \mathbf{Y}_{N} =\mathbf{\Phi}_{N}\boldsymbol{\theta}^{\star}+\mathbf{V}_{N}, \quad \mathbf{\Phi}_{N} = \begin{bmatrix} \boldsymbol{\phi}^{T}(0)\\ \boldsymbol{\phi}^{T}(1)\\ \vdots\\ \boldsymbol{\phi}^{T}(N-1) \end{bmatrix}, \quad \mathbf{Y}_{N} = \begin{bmatrix} y(1)\\y(2)\\\vdots\\y(N) \end{bmatrix}. \]
Batch least squares chooses a single parameter vector for the complete record:
\[ \widehat{\boldsymbol{\theta} }_{N}^{\mathrm{LS} } = \arg\min_{\boldsymbol{\theta} } J_{N}(\boldsymbol{\theta}), \qquad J_{N}(\boldsymbol{\theta}) = \frac{1}{2} \left\| \mathbf{Y}_{N}-\mathbf{\Phi}_{N}\boldsymbol{\theta} \right\|_{2}^{2}. \]
The experiment can often be designed specifically for information quality: multisine, PRBS, filtered noise, stepped inputs, repeated trials, or frequency sweeps may be used subject to amplitude, slew-rate, and safety constraints. Model order, delay, noise structure, residual whiteness, cross-validation, and uncertainty quantification are central outputs. A full offline system-identification course therefore studies substantially more than the parameter update needed by an STR.
Primary deliverable: a validated mathematical model and an account of its uncertainty and domain of validity. The controller may be designed later, possibly by another engineer or for several different control objectives.
4. Online Identification in an STR: Control Is the Primary Product
An indirect STR repeatedly performs three coupled operations:
\[ \boxed{ \text{estimate plant parameters} \;\Longrightarrow\; \text{synthesize controller parameters} \;\Longrightarrow\; \text{apply feedback} } \]
The estimator is embedded in the feedback loop. At each sample, a generic recursion has the form
\[ \widehat{\boldsymbol{\theta} }(k) = \widehat{\boldsymbol{\theta} }(k-1) + \mathbf{K}(k)\varepsilon(k), \]
and the controller coefficients are recalculated through a design map \( \boldsymbol{\rho}(k)=\mathcal{C}( \widehat{\boldsymbol{\theta} }(k)) \). The applied input is
\[ u(k)=\kappa\!\left( r(k),y(k),\boldsymbol{\rho}(k) \right). \]
The identifier is therefore not an isolated model-building module. Its transient behavior affects the control signal immediately. A temporary sign error in an estimated input gain, an ill-conditioned covariance matrix, or an aggressive forgetting factor can create excessive control action before conventional offline validation would even begin.
Primary deliverable: acceptable closed-loop behavior despite uncertainty and change. Parameter estimates are internal coordinates used to construct the controller; they are not automatically scientifically meaningful physical estimates.
5. Formal Comparison
| Aspect | Offline System Identification | Online Identification in an STR |
|---|---|---|
| Primary objective | Construct and validate a model | Maintain closed-loop performance |
| Data availability | Finite stored record, often revisited many times | Sequential stream, processed under real-time constraints |
| Experiment | Can be separated from normal operation | Usually occurs during regulation or tracking |
| Input source | Designed excitation or normal-operation data | Feedback controller, reference, disturbances, optional probing |
| Estimator–controller coupling | Usually absent during data collection | Strong and bidirectional |
| Model structure | May be rich and selected through validation | Usually parsimonious and control-oriented |
| Typical criterion | Prediction/simulation error and statistical diagnostics | Tracking, regulation, boundedness, safety, and adaptation quality |
| Parameter changes | Often assumes one stationary experiment | May use forgetting or resetting to follow time variation |
| Computational budget | Can use repeated optimization and model comparison | Strict per-sample time and memory limits |
| Validation | Independent validation data are standard | Validation is intertwined with safe closed-loop monitoring |
| Failure consequence | Poor model can be rejected before deployment | Poor estimate can immediately produce a poor control input |
6. Batch Least Squares: Derivation and Statistical Meaning
Differentiate the quadratic criterion with respect to \( \boldsymbol{\theta} \):
\[ \begin{aligned} J_{N}(\boldsymbol{\theta}) &= \frac{1}{2} (\mathbf{Y}_{N}-\mathbf{\Phi}_{N}\boldsymbol{\theta})^{T} (\mathbf{Y}_{N}-\mathbf{\Phi}_{N}\boldsymbol{\theta}),\\ \nabla_{\boldsymbol{\theta} }J_{N} &= -\mathbf{\Phi}_{N}^{T} (\mathbf{Y}_{N}-\mathbf{\Phi}_{N}\boldsymbol{\theta}). \end{aligned} \]
The stationary point satisfies the normal equation
\[ \mathbf{\Phi}_{N}^{T}\mathbf{\Phi}_{N} \widehat{\boldsymbol{\theta} }_{N} = \mathbf{\Phi}_{N}^{T}\mathbf{Y}_{N}. \]
If \( \mathbf{\Phi}_{N}^{T}\mathbf{\Phi}_{N} \) is positive definite, the criterion is strictly convex and
\[ \boxed{ \widehat{\boldsymbol{\theta} }_{N}^{\mathrm{LS} } = (\mathbf{\Phi}_{N}^{T}\mathbf{\Phi}_{N})^{-1} \mathbf{\Phi}_{N}^{T}\mathbf{Y}_{N} }. \]
Substitution of \( \mathbf{Y}_{N}=\mathbf{\Phi}_{N} \boldsymbol{\theta}^{\star}+\mathbf{V}_{N} \) gives the estimation error:
\[ \widetilde{\boldsymbol{\theta} }_{N} = \widehat{\boldsymbol{\theta} }_{N} -\boldsymbol{\theta}^{\star} = (\mathbf{\Phi}_{N}^{T}\mathbf{\Phi}_{N})^{-1} \mathbf{\Phi}_{N}^{T}\mathbf{V}_{N}. \]
If the noise has zero conditional mean and covariance \( \sigma_{v}^{2}\mathbf{I} \), then conditional on the regressor matrix,
\[ \mathbb{E}\!\left[ \widetilde{\boldsymbol{\theta} }_{N} \mid\mathbf{\Phi}_{N} \right] =\mathbf{0}, \qquad \operatorname{Cov}\!\left( \widehat{\boldsymbol{\theta} }_{N} \mid\mathbf{\Phi}_{N} \right) = \sigma_{v}^{2} (\mathbf{\Phi}_{N}^{T}\mathbf{\Phi}_{N})^{-1}. \]
This formula exposes the purpose of an identification experiment: enlarge and shape the information matrix \( \mathbf{\Phi}_{N}^{T}\mathbf{\Phi}_{N} \). Rich input design reduces uncertainty in parameter directions that the experiment excites.
7. From Batch LS to Recursive LS: Why the Algebra Fits an STR
Define the weighted information matrix and data vector
\[ \mathbf{R}(k) = \sum_{i=1}^{k} \lambda^{k-i} \boldsymbol{\phi}(i-1)\boldsymbol{\phi}^{T}(i-1), \qquad \mathbf{q}(k) = \sum_{i=1}^{k} \lambda^{k-i} \boldsymbol{\phi}(i-1)y(i), \]
where \( 0<\lambda\leq 1 \) is a forgetting factor. The weighted least-squares solution is
\[ \widehat{\boldsymbol{\theta} }(k) = \mathbf{R}^{-1}(k)\mathbf{q}(k). \]
Let \( \mathbf{P}(k)=\mathbf{R}^{-1}(k) \). Since
\[ \mathbf{R}(k) = \lambda\mathbf{R}(k-1) + \boldsymbol{\phi}(k-1)\boldsymbol{\phi}^{T}(k-1), \]
the matrix-inversion lemma produces the recursion
\[ \mathbf{K}(k) = \frac{ \mathbf{P}(k-1)\boldsymbol{\phi}(k-1) }{ \lambda+ \boldsymbol{\phi}^{T}(k-1) \mathbf{P}(k-1) \boldsymbol{\phi}(k-1) }, \]
\[ \widehat{\boldsymbol{\theta} }(k) = \widehat{\boldsymbol{\theta} }(k-1) + \mathbf{K}(k) \left[ y(k)-\boldsymbol{\phi}^{T}(k-1) \widehat{\boldsymbol{\theta} }(k-1) \right], \]
\[ \mathbf{P}(k) = \frac{1}{\lambda} \left[ \mathbf{P}(k-1) - \mathbf{K}(k) \boldsymbol{\phi}^{T}(k-1) \mathbf{P}(k-1) \right]. \]
These equations require only the previous estimate, previous covariance, current regressor, and newest output. That fixed-memory sequential form is why RLS is structurally compatible with an STR. It does not mean that every RLS implementation is safe for adaptive control; projection, normalization, covariance conditioning, gain limits, and supervisory logic may still be necessary.
8. Certainty Equivalence and the Closed-Loop Error Equation
Consider the scalar plant
\[ y(k+1)=a^{\star}y(k)+b^{\star}u(k)+v(k+1) = \boldsymbol{\phi}^{T}(k)\boldsymbol{\theta}^{\star}+v(k+1), \]
with \( \boldsymbol{\phi}(k)=[y(k)\;\;u(k)]^{T} \) and \( \boldsymbol{\theta}^{\star}=[a^{\star}\;\;b^{\star}]^{T} \). Let the desired first-order reference model be
\[ y_{m}(k+1) = p_{d}y_{m}(k)+(1-p_{d})r(k), \qquad |p_{d}|<1. \]
A certainty-equivalent controller chooses \( u(k) \) so the estimated plant obeys the desired equation:
\[ \widehat{a}(k)y(k)+\widehat{b}(k)u(k) = p_{d}y(k)+(1-p_{d})r(k), \]
\[ u(k) = \frac{ [p_{d}-\widehat{a}(k)]y(k) +(1-p_{d})r(k) }{ \widehat{b}(k) }. \]
Define \( e(k)=y(k)-y_{m}(k) \) and \( \widetilde{\boldsymbol{\theta} }(k) =\boldsymbol{\theta}^{\star} -\widehat{\boldsymbol{\theta} }(k) \). Add and subtract the estimated model:
\[ \begin{aligned} y(k+1) &= \boldsymbol{\phi}^{T}(k) \widehat{\boldsymbol{\theta} }(k) + \boldsymbol{\phi}^{T}(k) \widetilde{\boldsymbol{\theta} }(k) +v(k+1)\\ &= p_{d}y(k)+(1-p_{d})r(k) + \boldsymbol{\phi}^{T}(k) \widetilde{\boldsymbol{\theta} }(k) +v(k+1). \end{aligned} \]
Subtracting the reference-model equation gives
\[ \boxed{ e(k+1) = p_{d}e(k) + \boldsymbol{\phi}^{T}(k) \widetilde{\boldsymbol{\theta} }(k) +v(k+1) }. \]
This identity is central. The controller does not require every component of the parameter vector to converge independently; it requires the regressor-weighted parameter error to become sufficiently small while all signals remain bounded. Consequently, closed-loop tracking can be good even when the parameter estimate is not equal to the physical parameter vector.
9. Closed-Loop Data, Excitation, and the Regulation–Learning Conflict
In offline open-loop identification, the experimenter can choose \( u(k) \) to make the information matrix well conditioned. In an STR, the control input is generated mainly to reduce tracking error. If regulation succeeds, the output and input may become nearly constant, and the regressor loses rank.
A finite-window excitation condition is
\[ \sum_{i=k}^{k+L-1} \boldsymbol{\phi}(i)\boldsymbol{\phi}^{T}(i) \succeq \alpha\mathbf{I}, \qquad \alpha>0. \]
If the reference is constant and disturbances are negligible, successful regulation may produce \( \boldsymbol{\phi}(k)\approx\overline{\boldsymbol{\phi} } \). Then
\[ \sum_{i=k}^{k+L-1} \boldsymbol{\phi}(i)\boldsymbol{\phi}^{T}(i) \approx L\overline{\boldsymbol{\phi} } \overline{\boldsymbol{\phi} }^{T}, \]
which has rank one regardless of the dimension of \( \boldsymbol{\theta} \). Full parameter identification is then impossible from that operating condition.
Adding a probing signal may improve identifiability, but it also perturbs the controlled plant. This is the regulation–learning conflict:
\[ J_{\mathrm{total} } = J_{\mathrm{control} } + \mu J_{\mathrm{experiment} }, \qquad \mu>0, \]
where one term penalizes tracking error and control effort while the other rewards informative data. Basic certainty-equivalent STRs usually ignore the future information value of the current control action. More advanced dual-control ideas explicitly recognize that the input both controls and probes the plant.
flowchart TD
A["Controller reduces tracking error"] --> B["Input and output become less variable"]
B --> C["Regressor information decreases"]
C --> D["Parameter uncertainty may stop shrinking"]
D --> E["Plant changes or new operating point appears"]
E --> F["Old estimate may no longer support desired performance"]
F --> G["Use reference variation, safe probing, \nforgetting, reset, or supervision"]
G --> A
10. Prediction Accuracy Is Not Identical to Control Relevance
A conventional prediction-error criterion weights model mismatch by the spectrum present in the identification data. In simplified frequency notation,
\[ J_{\mathrm{id} } \approx \frac{1}{2\pi} \int_{-\pi}^{\pi} W_{\mathrm{id} }(\mathrm{e}^{\mathrm{j}\omega}) \left| G(\mathrm{e}^{\mathrm{j}\omega}) - \widehat{G}(\mathrm{e}^{\mathrm{j}\omega}) \right|^{2} d\omega. \]
When the model is used to design a controller, the important frequency regions are instead weighted by reference spectra, disturbance spectra, sensitivity functions, complementary sensitivity functions, actuator limits, and robustness margins. A local control-oriented approximation has the form
\[ \Delta J_{\mathrm{cl} } \approx \frac{1}{2\pi} \int_{-\pi}^{\pi} W_{\mathrm{cl} }(\mathrm{e}^{\mathrm{j}\omega}) \left| G(\mathrm{e}^{\mathrm{j}\omega}) - \widehat{G}(\mathrm{e}^{\mathrm{j}\omega}) \right|^{2} d\omega. \]
Thus a globally accurate model is not always necessary for a specific controller, and a low average prediction error does not guarantee good control. Offline identification can—and often should—be control-oriented when the intended controller is known. The distinction is not that offline identification ignores control, but that offline identification still treats model construction and validation as an explicit, separable engineering task.
11. Worked Example: Frozen Offline Model versus Online STR
The implementations below use
\[ y(k+1)=a(k)y(k)+0.5u(k)+v(k+1), \]
with \( a(k)=0.65 \) initially and \( a(k)=0.82 \) after sample 250. A separate persistently exciting batch first estimates \( [a\;\;b]^{T} \). Two controllers are then compared:
- a controller whose offline estimate is frozen;
- an indirect STR using RLS, a small probing signal, input saturation, and simple parameter projection.
Both controllers use the desired pole \( p_{d}=0.25 \). The expected qualitative result is that both perform similarly before the change, while the online STR adjusts after the change and generally reduces post-change tracking error. This numerical example illustrates architecture; it is not a proof of global stability.
12. Python Implementation
The Python version uses NumPy for linear algebra and
Matplotlib for visualization. The batch solver uses
numpy.linalg.lstsq rather than explicitly inverting the
normal matrix.
Chapter11_Lesson5.py
# Chapter11_Lesson5.py
# Offline batch identification versus online identification inside an STR.
# Dependencies: numpy and matplotlib.
from __future__ import annotations
import csv
from dataclasses import dataclass
import matplotlib.pyplot as plt
import numpy as np
@dataclass
class RLS:
theta: np.ndarray
covariance: np.ndarray
forgetting_factor: float = 0.995
def update(self, phi: np.ndarray, measured_output: float) -> float:
"""Perform one RLS update and return the one-step prediction error."""
phi = np.asarray(phi, dtype=float)
denominator = self.forgetting_factor + phi @ self.covariance @ phi
gain = (self.covariance @ phi) / denominator
prediction_error = measured_output - phi @ self.theta
self.theta = self.theta + gain * prediction_error
self.covariance = (
self.covariance - np.outer(gain, phi) @ self.covariance
) / self.forgetting_factor
return float(prediction_error)
def batch_least_squares(phi_matrix: np.ndarray, outputs: np.ndarray) -> np.ndarray:
"""Solve min ||Y - Phi theta||_2 without explicitly inverting Phi^T Phi."""
estimate, *_ = np.linalg.lstsq(phi_matrix, outputs, rcond=None)
return estimate
def reference_signal(k: int) -> float:
if k < 100:
return 1.0
if k < 200:
return -0.5
if k < 320:
return 0.8
return -0.2
def true_a(k: int) -> float:
return 0.65 if k < 250 else 0.82
def certainty_equivalent_control(
y: float,
reference: float,
theta_hat: np.ndarray,
desired_pole: float,
dither: float = 0.0,
limit: float = 3.0,
) -> float:
a_hat, b_hat = theta_hat
safe_b = b_hat if abs(b_hat) >= 0.08 else np.copysign(0.08, b_hat or 1.0)
u = ((desired_pole - a_hat) * y + (1.0 - desired_pole) * reference) / safe_b
return float(np.clip(u + dither, -limit, limit))
def main() -> None:
rng = np.random.default_rng(11)
b_true = 0.50
noise_std = 0.015
desired_pole = 0.25
# Offline identification experiment: collect a separate, persistently exciting batch.
n_identification = 180
y_id = np.zeros(n_identification + 1)
u_id = rng.uniform(-1.0, 1.0, size=n_identification)
phi_rows = []
targets = []
for k in range(n_identification):
y_id[k + 1] = (
0.65 * y_id[k] + b_true * u_id[k] + rng.normal(0.0, noise_std)
)
phi_rows.append([y_id[k], u_id[k]])
targets.append(y_id[k + 1])
theta_offline = batch_least_squares(
np.asarray(phi_rows), np.asarray(targets)
)
print(f"Offline estimate [a, b] = {theta_offline}")
# Two controllers see the same reference and plant change:
# 1) frozen controller based on the offline model;
# 2) indirect STR with online RLS and small probing dither.
n_control = 450
y_frozen = np.zeros(n_control + 1)
y_str = np.zeros(n_control + 1)
u_frozen = np.zeros(n_control)
u_str = np.zeros(n_control)
references = np.array([reference_signal(k) for k in range(n_control)])
a_values = np.array([true_a(k) for k in range(n_control)])
theta_history = np.zeros((n_control + 1, 2))
rls = RLS(
theta=theta_offline.copy(),
covariance=25.0 * np.eye(2),
forgetting_factor=0.995,
)
theta_history[0] = rls.theta
for k in range(n_control):
r = references[k]
u_frozen[k] = certainty_equivalent_control(
y_frozen[k], r, theta_offline, desired_pole
)
y_frozen[k + 1] = (
a_values[k] * y_frozen[k]
+ b_true * u_frozen[k]
+ rng.normal(0.0, noise_std)
)
dither = 0.035 * np.sin(0.19 * k) + 0.020 * np.sin(0.071 * k)
u_str[k] = certainty_equivalent_control(
y_str[k], r, rls.theta, desired_pole, dither=dither
)
y_str[k + 1] = (
a_values[k] * y_str[k]
+ b_true * u_str[k]
+ rng.normal(0.0, noise_std)
)
rls.update(np.array([y_str[k], u_str[k]]), y_str[k + 1])
rls.theta[0] = np.clip(rls.theta[0], -0.98, 0.98)
rls.theta[1] = np.clip(rls.theta[1], 0.08, 1.50)
theta_history[k + 1] = rls.theta
post_change = slice(270, n_control)
rmse_frozen = np.sqrt(
np.mean((y_frozen[1:][post_change] - references[post_change]) ** 2)
)
rmse_str = np.sqrt(
np.mean((y_str[1:][post_change] - references[post_change]) ** 2)
)
print(f"Final online estimate [a, b] = {rls.theta}")
print(f"Post-change RMSE, frozen controller = {rmse_frozen:.5f}")
print(f"Post-change RMSE, online STR = {rmse_str:.5f}")
with open("Chapter11_Lesson5_results.csv", "w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerow(
["k", "reference", "a_true", "y_frozen", "y_str", "a_hat", "b_hat"]
)
for k in range(n_control):
writer.writerow(
[
k,
references[k],
a_values[k],
y_frozen[k + 1],
y_str[k + 1],
theta_history[k + 1, 0],
theta_history[k + 1, 1],
]
)
time = np.arange(n_control)
plt.figure(figsize=(10, 5))
plt.plot(time, references, "--", label="reference")
plt.plot(time, y_frozen[1:], label="frozen offline model")
plt.plot(time, y_str[1:], label="online STR")
plt.axvline(250, linestyle=":", label="plant parameter change")
plt.xlabel("sample k")
plt.ylabel("output")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
plt.figure(figsize=(10, 4))
plt.plot(time, a_values, "--", label="true a")
plt.plot(time, theta_history[1:, 0], label="online estimate a_hat")
plt.xlabel("sample k")
plt.ylabel("parameter")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
if __name__ == "__main__":
main()
13. C++ Implementation
The C++17 implementation uses only the standard library. A closed-form two-parameter batch solution and a fixed-size RLS covariance matrix make the real-time data path explicit.
Chapter11_Lesson5.cpp
// Chapter11_Lesson5.cpp
// Offline batch identification versus online identification inside an STR.
// Build: g++ -std=c++17 -O2 Chapter11_Lesson5.cpp -o Chapter11_Lesson5
#include <algorithm>
#include <array>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <random>
#include <stdexcept>
#include <vector>
using Vec2 = std::array<double, 2>;
using Mat2 = std::array<std::array<double, 2>, 2>;
double dot(const Vec2& x, const Vec2& y) {
return x[0] * y[0] + x[1] * y[1];
}
Vec2 matVec(const Mat2& A, const Vec2& x) {
return {
A[0][0] * x[0] + A[0][1] * x[1],
A[1][0] * x[0] + A[1][1] * x[1]
};
}
Vec2 batchLeastSquares(
const std::vector<Vec2>& phi,
const std::vector<double>& outputs
) {
double s00 = 0.0, s01 = 0.0, s11 = 0.0;
double q0 = 0.0, q1 = 0.0;
for (std::size_t i = 0; i < phi.size(); ++i) {
s00 += phi[i][0] * phi[i][0];
s01 += phi[i][0] * phi[i][1];
s11 += phi[i][1] * phi[i][1];
q0 += phi[i][0] * outputs[i];
q1 += phi[i][1] * outputs[i];
}
const double determinant = s00 * s11 - s01 * s01;
if (std::abs(determinant) < 1.0e-12) {
throw std::runtime_error("Identification data are not sufficiently exciting.");
}
return {
(s11 * q0 - s01 * q1) / determinant,
(-s01 * q0 + s00 * q1) / determinant
};
}
struct RLS {
Vec2 theta;
Mat2 covariance;
double lambda;
double update(const Vec2& phi, double measuredOutput) {
const Vec2 pPhi = matVec(covariance, phi);
const double denominator = lambda + dot(phi, pPhi);
const Vec2 gain{pPhi[0] / denominator, pPhi[1] / denominator};
const double error = measuredOutput - dot(phi, theta);
theta[0] += gain[0] * error;
theta[1] += gain[1] * error;
Mat2 newCovariance{};
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
newCovariance[i][j] =
(covariance[i][j]
- gain[i] * (phi[0] * covariance[0][j]
+ phi[1] * covariance[1][j]))
/ lambda;
}
}
covariance = newCovariance;
return error;
}
};
double referenceSignal(int k) {
if (k < 100) return 1.0;
if (k < 200) return -0.5;
if (k < 320) return 0.8;
return -0.2;
}
double trueA(int k) {
return (k < 250) ? 0.65 : 0.82;
}
double safeInputGain(double bHat) {
if (std::abs(bHat) >= 0.08) return bHat;
return (bHat < 0.0) ? -0.08 : 0.08;
}
double controller(
double y,
double reference,
const Vec2& thetaHat,
double desiredPole,
double dither = 0.0
) {
const double bSafe = safeInputGain(thetaHat[1]);
const double raw =
((desiredPole - thetaHat[0]) * y
+ (1.0 - desiredPole) * reference) / bSafe
+ dither;
return std::clamp(raw, -3.0, 3.0);
}
int main() {
std::mt19937 generator(11);
std::uniform_real_distribution<double> inputDistribution(-1.0, 1.0);
std::normal_distribution<double> noise(0.0, 0.015);
constexpr double bTrue = 0.50;
constexpr double desiredPole = 0.25;
constexpr int identificationSamples = 180;
constexpr int controlSamples = 450;
std::vector<Vec2> phi;
std::vector<double> targets;
phi.reserve(identificationSamples);
targets.reserve(identificationSamples);
double yIdentification = 0.0;
for (int k = 0; k < identificationSamples; ++k) {
const double u = inputDistribution(generator);
const double nextY = 0.65 * yIdentification + bTrue * u + noise(generator);
phi.push_back({yIdentification, u});
targets.push_back(nextY);
yIdentification = nextY;
}
const Vec2 thetaOffline = batchLeastSquares(phi, targets);
std::cout << std::fixed << std::setprecision(6);
std::cout << "Offline estimate [a, b] = ["
<< thetaOffline[0] << ", " << thetaOffline[1] << "]\n";
RLS rls{
thetaOffline,
{ { {25.0, 0.0}, {0.0, 25.0} }},
0.995
};
double yFrozen = 0.0;
double yStr = 0.0;
double squaredErrorFrozen = 0.0;
double squaredErrorStr = 0.0;
int postChangeCount = 0;
std::ofstream csv("Chapter11_Lesson5_results.csv");
csv << "k,reference,a_true,y_frozen,y_str,a_hat,b_hat\n";
for (int k = 0; k < controlSamples; ++k) {
const double reference = referenceSignal(k);
const double a = trueA(k);
const double uFrozen =
controller(yFrozen, reference, thetaOffline, desiredPole);
yFrozen = a * yFrozen + bTrue * uFrozen + noise(generator);
const double dither =
0.035 * std::sin(0.19 * k) + 0.020 * std::sin(0.071 * k);
const double uStr =
controller(yStr, reference, rls.theta, desiredPole, dither);
const double nextYStr = a * yStr + bTrue * uStr + noise(generator);
rls.update({yStr, uStr}, nextYStr);
rls.theta[0] = std::clamp(rls.theta[0], -0.98, 0.98);
rls.theta[1] = std::clamp(rls.theta[1], 0.08, 1.50);
yStr = nextYStr;
if (k >= 270) {
squaredErrorFrozen += std::pow(yFrozen - reference, 2);
squaredErrorStr += std::pow(yStr - reference, 2);
++postChangeCount;
}
csv << k << ',' << reference << ',' << a << ','
<< yFrozen << ',' << yStr << ','
<< rls.theta[0] << ',' << rls.theta[1] << '\n';
}
const double rmseFrozen =
std::sqrt(squaredErrorFrozen / postChangeCount);
const double rmseStr =
std::sqrt(squaredErrorStr / postChangeCount);
std::cout << "Final online estimate [a, b] = ["
<< rls.theta[0] << ", " << rls.theta[1] << "]\n";
std::cout << "Post-change RMSE, frozen controller = "
<< rmseFrozen << '\n';
std::cout << "Post-change RMSE, online STR = "
<< rmseStr << '\n';
return 0;
}
14. Java Implementation
The Java implementation uses standard arrays, Random, and
PrintWriter. No external numerical package is required for
this two-parameter demonstration.
Chapter11_Lesson5.java
// Chapter11_Lesson5.java
// Offline batch identification versus online identification inside an STR.
// Build and run:
// javac Chapter11_Lesson5.java
// java Chapter11_Lesson5
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Random;
public class Chapter11_Lesson5 {
private static final double B_TRUE = 0.50;
private static final double DESIRED_POLE = 0.25;
private static final class RLS {
private final double[] theta = new double[2];
private final double[][] covariance = new double[2][2];
private final double lambda;
RLS(double[] initialTheta, double initialCovariance, double forgettingFactor) {
theta[0] = initialTheta[0];
theta[1] = initialTheta[1];
covariance[0][0] = initialCovariance;
covariance[1][1] = initialCovariance;
lambda = forgettingFactor;
}
double update(double[] phi, double measuredOutput) {
double pPhi0 = covariance[0][0] * phi[0] + covariance[0][1] * phi[1];
double pPhi1 = covariance[1][0] * phi[0] + covariance[1][1] * phi[1];
double denominator = lambda + phi[0] * pPhi0 + phi[1] * pPhi1;
double gain0 = pPhi0 / denominator;
double gain1 = pPhi1 / denominator;
double prediction = phi[0] * theta[0] + phi[1] * theta[1];
double error = measuredOutput - prediction;
theta[0] += gain0 * error;
theta[1] += gain1 * error;
double[][] old = {
{covariance[0][0], covariance[0][1]},
{covariance[1][0], covariance[1][1]}
};
for (int i = 0; i < 2; i++) {
double gain = (i == 0) ? gain0 : gain1;
for (int j = 0; j < 2; j++) {
double phiTPColumn = phi[0] * old[0][j] + phi[1] * old[1][j];
covariance[i][j] = (old[i][j] - gain * phiTPColumn) / lambda;
}
}
return error;
}
}
private static double[] batchLeastSquares(double[][] phi, double[] outputs) {
double s00 = 0.0;
double s01 = 0.0;
double s11 = 0.0;
double q0 = 0.0;
double q1 = 0.0;
for (int i = 0; i < outputs.length; i++) {
s00 += phi[i][0] * phi[i][0];
s01 += phi[i][0] * phi[i][1];
s11 += phi[i][1] * phi[i][1];
q0 += phi[i][0] * outputs[i];
q1 += phi[i][1] * outputs[i];
}
double determinant = s00 * s11 - s01 * s01;
if (Math.abs(determinant) < 1.0e-12) {
throw new IllegalStateException(
"Identification data are not sufficiently exciting."
);
}
return new double[] {
(s11 * q0 - s01 * q1) / determinant,
(-s01 * q0 + s00 * q1) / determinant
};
}
private static double referenceSignal(int k) {
if (k < 100) return 1.0;
if (k < 200) return -0.5;
if (k < 320) return 0.8;
return -0.2;
}
private static double trueA(int k) {
return k < 250 ? 0.65 : 0.82;
}
private static double clamp(double value, double lower, double upper) {
return Math.max(lower, Math.min(upper, value));
}
private static double controller(
double y,
double reference,
double[] thetaHat,
double dither
) {
double bSafe = Math.abs(thetaHat[1]) >= 0.08
? thetaHat[1]
: Math.copySign(0.08, thetaHat[1] == 0.0 ? 1.0 : thetaHat[1]);
double u = (
(DESIRED_POLE - thetaHat[0]) * y
+ (1.0 - DESIRED_POLE) * reference
) / bSafe + dither;
return clamp(u, -3.0, 3.0);
}
public static void main(String[] args) throws Exception {
Locale.setDefault(Locale.US);
Random random = new Random(11);
int identificationSamples = 180;
double[][] phi = new double[identificationSamples][2];
double[] targets = new double[identificationSamples];
double yIdentification = 0.0;
for (int k = 0; k < identificationSamples; k++) {
double u = -1.0 + 2.0 * random.nextDouble();
double nextY =
0.65 * yIdentification
+ B_TRUE * u
+ 0.015 * random.nextGaussian();
phi[k][0] = yIdentification;
phi[k][1] = u;
targets[k] = nextY;
yIdentification = nextY;
}
double[] thetaOffline = batchLeastSquares(phi, targets);
System.out.printf(
"Offline estimate [a, b] = [%.6f, %.6f]%n",
thetaOffline[0],
thetaOffline[1]
);
RLS rls = new RLS(thetaOffline, 25.0, 0.995);
int controlSamples = 450;
double yFrozen = 0.0;
double yStr = 0.0;
double squaredErrorFrozen = 0.0;
double squaredErrorStr = 0.0;
int postChangeCount = 0;
try (
PrintWriter csv = new PrintWriter(
"Chapter11_Lesson5_results.csv",
StandardCharsets.UTF_8
)
) {
csv.println("k,reference,a_true,y_frozen,y_str,a_hat,b_hat");
for (int k = 0; k < controlSamples; k++) {
double reference = referenceSignal(k);
double a = trueA(k);
double uFrozen = controller(
yFrozen,
reference,
thetaOffline,
0.0
);
yFrozen =
a * yFrozen
+ B_TRUE * uFrozen
+ 0.015 * random.nextGaussian();
double dither =
0.035 * Math.sin(0.19 * k)
+ 0.020 * Math.sin(0.071 * k);
double uStr = controller(yStr, reference, rls.theta, dither);
double nextYStr =
a * yStr
+ B_TRUE * uStr
+ 0.015 * random.nextGaussian();
rls.update(new double[] {yStr, uStr}, nextYStr);
rls.theta[0] = clamp(rls.theta[0], -0.98, 0.98);
rls.theta[1] = clamp(rls.theta[1], 0.08, 1.50);
yStr = nextYStr;
if (k >= 270) {
squaredErrorFrozen += Math.pow(yFrozen - reference, 2);
squaredErrorStr += Math.pow(yStr - reference, 2);
postChangeCount++;
}
csv.printf(
"%d,%.9f,%.9f,%.9f,%.9f,%.9f,%.9f%n",
k,
reference,
a,
yFrozen,
yStr,
rls.theta[0],
rls.theta[1]
);
}
}
double rmseFrozen = Math.sqrt(squaredErrorFrozen / postChangeCount);
double rmseStr = Math.sqrt(squaredErrorStr / postChangeCount);
System.out.printf(
"Final online estimate [a, b] = [%.6f, %.6f]%n",
rls.theta[0],
rls.theta[1]
);
System.out.printf(
"Post-change RMSE, frozen controller = %.6f%n",
rmseFrozen
);
System.out.printf(
"Post-change RMSE, online STR = %.6f%n",
rmseStr
);
}
}
15. MATLAB and Simulink Implementation
The MATLAB script uses matrix left division for batch LS and implements RLS directly. Its final comments specify a Simulink realization with a discrete plant, MATLAB Function blocks for RLS and control, Unit Delay states, saturation, probing, scopes, and workspace logging.
Chapter11_Lesson5.m
% Chapter11_Lesson5.m
% Offline batch identification versus online identification inside an STR.
% Uses base MATLAB. The script also shows how the same equations map to Simulink.
clear; clc; close all;
rng(11);
bTrue = 0.50;
noiseStd = 0.015;
desiredPole = 0.25;
%% Offline identification experiment
nIdentification = 180;
yId = zeros(nIdentification + 1, 1);
uId = -1 + 2 * rand(nIdentification, 1);
Phi = zeros(nIdentification, 2);
Y = zeros(nIdentification, 1);
for k = 1:nIdentification
yId(k + 1) = 0.65 * yId(k) + bTrue * uId(k) ...
+ noiseStd * randn;
Phi(k, :) = [yId(k), uId(k)];
Y(k) = yId(k + 1);
end
% Numerically preferable to inv(Phi' * Phi) * Phi' * Y.
thetaOffline = Phi \ Y;
fprintf('Offline estimate [a, b] = [%.6f, %.6f]\n', ...
thetaOffline(1), thetaOffline(2));
%% Closed-loop comparison
nControl = 450;
reference = zeros(nControl, 1);
aTrue = zeros(nControl, 1);
for k = 1:nControl
index = k - 1;
if index < 100
reference(k) = 1.0;
elseif index < 200
reference(k) = -0.5;
elseif index < 320
reference(k) = 0.8;
else
reference(k) = -0.2;
end
if index < 250
aTrue(k) = 0.65;
else
aTrue(k) = 0.82;
end
end
yFrozen = zeros(nControl + 1, 1);
ySTR = zeros(nControl + 1, 1);
uFrozen = zeros(nControl, 1);
uSTR = zeros(nControl, 1);
thetaHistory = zeros(nControl + 1, 2);
thetaOnline = thetaOffline;
P = 25 * eye(2);
lambda = 0.995;
thetaHistory(1, :) = thetaOnline';
for k = 1:nControl
index = k - 1;
% Controller using the frozen offline model.
bSafe = safeInputGain(thetaOffline(2));
uFrozen(k) = ((desiredPole - thetaOffline(1)) * yFrozen(k) ...
+ (1 - desiredPole) * reference(k)) / bSafe;
uFrozen(k) = min(max(uFrozen(k), -3), 3);
yFrozen(k + 1) = aTrue(k) * yFrozen(k) + bTrue * uFrozen(k) ...
+ noiseStd * randn;
% Indirect STR: estimate, synthesize, apply, and repeat.
dither = 0.035 * sin(0.19 * index) + 0.020 * sin(0.071 * index);
bSafe = safeInputGain(thetaOnline(2));
uSTR(k) = ((desiredPole - thetaOnline(1)) * ySTR(k) ...
+ (1 - desiredPole) * reference(k)) / bSafe + dither;
uSTR(k) = min(max(uSTR(k), -3), 3);
yNext = aTrue(k) * ySTR(k) + bTrue * uSTR(k) ...
+ noiseStd * randn;
phi = [ySTR(k); uSTR(k)];
gain = (P * phi) / (lambda + phi' * P * phi);
predictionError = yNext - phi' * thetaOnline;
thetaOnline = thetaOnline + gain * predictionError;
P = (P - gain * phi' * P) / lambda;
% Simple projection for safe demonstration.
thetaOnline(1) = min(max(thetaOnline(1), -0.98), 0.98);
thetaOnline(2) = min(max(thetaOnline(2), 0.08), 1.50);
ySTR(k + 1) = yNext;
thetaHistory(k + 1, :) = thetaOnline';
end
postChange = 271:nControl;
rmseFrozen = sqrt(mean( ...
(yFrozen(postChange + 1) - reference(postChange)).^2));
rmseSTR = sqrt(mean( ...
(ySTR(postChange + 1) - reference(postChange)).^2));
fprintf('Final online estimate [a, b] = [%.6f, %.6f]\n', ...
thetaOnline(1), thetaOnline(2));
fprintf('Post-change RMSE, frozen controller = %.6f\n', rmseFrozen);
fprintf('Post-change RMSE, online STR = %.6f\n', rmseSTR);
results = table( ...
(0:nControl-1)', reference, aTrue, yFrozen(2:end), ySTR(2:end), ...
thetaHistory(2:end, 1), thetaHistory(2:end, 2), ...
'VariableNames', ...
{'k', 'reference', 'a_true', 'y_frozen', 'y_str', 'a_hat', 'b_hat'});
writetable(results, 'Chapter11_Lesson5_results.csv');
figure;
plot(0:nControl-1, reference, '--', 'LineWidth', 1.2); hold on;
plot(0:nControl-1, yFrozen(2:end), 'LineWidth', 1.1);
plot(0:nControl-1, ySTR(2:end), 'LineWidth', 1.1);
xline(250, ':', 'plant parameter change');
grid on;
xlabel('sample k');
ylabel('output');
legend('reference', 'frozen offline model', 'online STR', ...
'Location', 'best');
title('Offline model versus online self-tuning regulator');
figure;
plot(0:nControl-1, aTrue, '--', 'LineWidth', 1.2); hold on;
plot(0:nControl-1, thetaHistory(2:end, 1), 'LineWidth', 1.1);
grid on;
xlabel('sample k');
ylabel('parameter');
legend('true a', 'online estimate a-hat', 'Location', 'best');
title('Online parameter tracking');
%% Simulink mapping
% Build the same experiment with:
% 1. Discrete-Time Plant subsystem: y(k+1) = a(k)y(k) + b u(k).
% 2. MATLAB Function block for the RLS equations.
% 3. MATLAB Function block for the certainty-equivalent controller.
% 4. Unit Delay blocks for y(k), theta-hat(k), and P(k).
% 5. Step/Signal Builder block for r(k), and a small dither source.
% 6. Saturation block on u(k), Scopes, and To Workspace blocks.
%
% Keep the estimator and controller in the same sample time. Use a triggered
% or enabled update only after y(k+1) is available, so the RLS regressor
% [y(k); u(k)] is paired with the correct measured target y(k+1).
function value = safeInputGain(bHat)
if abs(bHat) >= 0.08
value = bHat;
elseif bHat < 0
value = -0.08;
else
value = 0.08;
end
end
16. Wolfram Mathematica Implementation
The notebook uses LeastSquares, direct RLS matrix updates,
Export, and ListLinePlot. The downloadable
file is a textual Wolfram Notebook expression.
Chapter11_Lesson5.nb
Notebook[{
Cell["Chapter 11, Lesson 5: Offline Identification versus Online STR", "Title"],
Cell["Chapter11_Lesson5.nb", "Subtitle"],
Cell[BoxData[
"(* Offline batch identification versus online identification inside an STR. *)\n\
SeedRandom[11];\n\
bTrue = 0.50;\n\
noiseStd = 0.015;\n\
desiredPole = 0.25;\n\
nIdentification = 180;\n\
\n\
(* Offline experiment *)\n\
uId = RandomReal[{-1, 1}, nIdentification];\n\
yId = ConstantArray[0.0, nIdentification + 1];\n\
Do[\n\
yId[[k + 1]] = 0.65 yId[[k]] + bTrue uId[[k]] +\n\
RandomVariate[NormalDistribution[0, noiseStd]],\n\
{k, 1, nIdentification}\n\
];\n\
phi = Table[{yId[[k]], uId[[k]]}, {k, 1, nIdentification}];\n\
targets = yId[[2 ;;]];\n\
thetaOffline = LeastSquares[phi, targets];\n\
Print[\"Offline estimate [a, b] = \", N[thetaOffline, 6]];\n\
\n\
referenceSignal[k_] := Piecewise[{\n\
{1.0, k < 100},\n\
{-0.5, k < 200},\n\
{0.8, k < 320} }, -0.2];\n\
trueA[k_] := If[k < 250, 0.65, 0.82];\n\
safeInputGain[b_] := If[Abs[b] >= 0.08, b, If[b < 0, -0.08, 0.08]];\n\
clip[x_, lower_, upper_] := Min[upper, Max[lower, x]];\n\
controller[y_, r_, theta_, dither_: 0.0] := Module[{u},\n\
u = ((desiredPole - theta[[1]]) y +\n\
(1 - desiredPole) r)/safeInputGain[theta[[2]]] + dither;\n\
clip[u, -3.0, 3.0]\n\
];\n\
\n\
(* Closed-loop comparison *)\n\
nControl = 450;\n\
references = Table[referenceSignal[k], {k, 0, nControl - 1}];\n\
aValues = Table[trueA[k], {k, 0, nControl - 1}];\n\
yFrozen = ConstantArray[0.0, nControl + 1];\n\
ySTR = ConstantArray[0.0, nControl + 1];\n\
uFrozen = ConstantArray[0.0, nControl];\n\
uSTR = ConstantArray[0.0, nControl];\n\
thetaHistory = ConstantArray[{0.0, 0.0}, nControl + 1];\n\
thetaOnline = thetaOffline;\n\
pCov = 25.0 IdentityMatrix[2];\n\
lambda = 0.995;\n\
thetaHistory[[1]] = thetaOnline;\n\
\n\
Do[\n\
index = k - 1;\n\
r = references[[k]];\n\
a = aValues[[k]];\n\
\n\
uFrozen[[k]] = controller[yFrozen[[k]], r, thetaOffline];\n\
yFrozen[[k + 1]] = a yFrozen[[k]] + bTrue uFrozen[[k]] +\n\
RandomVariate[NormalDistribution[0, noiseStd]];\n\
\n\
dither = 0.035 Sin[0.19 index] + 0.020 Sin[0.071 index];\n\
uSTR[[k]] = controller[ySTR[[k]], r, thetaOnline, dither];\n\
yNext = a ySTR[[k]] + bTrue uSTR[[k]] +\n\
RandomVariate[NormalDistribution[0, noiseStd]];\n\
\n\
regressor = {ySTR[[k]], uSTR[[k]]};\n\
gain = (pCov.regressor)/(lambda + regressor.pCov.regressor);\n\
predictionError = yNext - regressor.thetaOnline;\n\
thetaOnline = thetaOnline + gain predictionError;\n\
pCov = (pCov - Outer[Times, gain, regressor].pCov)/lambda;\n\
thetaOnline[[1]] = clip[thetaOnline[[1]], -0.98, 0.98];\n\
thetaOnline[[2]] = clip[thetaOnline[[2]], 0.08, 1.50];\n\
\n\
ySTR[[k + 1]] = yNext;\n\
thetaHistory[[k + 1]] = thetaOnline,\n\
{k, 1, nControl}\n\
];\n\
\n\
postChange = Range[271, nControl];\n\
rmseFrozen = Sqrt[Mean[(yFrozen[[postChange + 1]] -\n\
references[[postChange]])^2]];\n\
rmseSTR = Sqrt[Mean[(ySTR[[postChange + 1]] -\n\
references[[postChange]])^2]];\n\
Print[\"Final online estimate [a, b] = \", N[thetaOnline, 6]];\n\
Print[\"Post-change RMSE, frozen controller = \", N[rmseFrozen, 6]];\n\
Print[\"Post-change RMSE, online STR = \", N[rmseSTR, 6]];\n\
\n\
results = Prepend[\n\
Table[{k - 1, references[[k]], aValues[[k]], yFrozen[[k + 1]],\n\
ySTR[[k + 1]], thetaHistory[[k + 1, 1]],\n\
thetaHistory[[k + 1, 2]]}, {k, 1, nControl}],\n\
{\"k\", \"reference\", \"a_true\", \"y_frozen\", \"y_str\", \"a_hat\", \"b_hat\"}\n\
];\n\
Export[\"Chapter11_Lesson5_results.csv\", results];\n\
\n\
trackingPlot = ListLinePlot[\n\
{references, yFrozen[[2 ;;]], ySTR[[2 ;;]]},\n\
PlotLegends -> {\"reference\", \"frozen offline model\", \"online STR\"},\n\
Frame -> True,\n\
FrameLabel -> {\"sample k\", \"output\"},\n\
GridLines -> { {250}, None},\n\
ImageSize -> Large\n\
];\n\
parameterPlot = ListLinePlot[\n\
{aValues, thetaHistory[[2 ;;, 1]]},\n\
PlotLegends -> {\"true a\", \"online estimate a-hat\"},\n\
Frame -> True,\n\
FrameLabel -> {\"sample k\", \"parameter\"},\n\
ImageSize -> Large\n\
];\n\
Column[{trackingPlot, parameterPlot}]\n"
], "Input"]
},
WindowTitle -> "Chapter11_Lesson5",
StyleDefinitions -> "Default.nb"
]
17. Practical Boundary: What Belongs in Which Course?
Adaptive Control course emphasis:
- how the estimate enters the control law;
- boundedness of states, parameters, and control signals;
- tracking and regulation under uncertainty;
- persistent excitation versus mere tracking convergence;
- projection, leakage, dead zones, normalization, and supervision;
- time-scale interaction between estimator and controller;
- safe behavior during poor excitation or parameter transients.
Offline System Identification course emphasis:
- model classes such as ARX, ARMAX, OE, Box–Jenkins, and state space;
- order and delay selection;
- prediction-error, subspace, instrumental-variable, and Bayesian methods;
- experiment design and spectral analysis;
- noise-model identification and statistical efficiency;
- residual tests, uncertainty sets, validation, and model reduction;
- open-loop and closed-loop identifiability in depth.
The overlap is real but asymmetric. Adaptive control borrows an estimator to serve a feedback objective. System identification develops the theory and practice of learning dynamical models, whether or not those models are used for adaptive feedback.
18. Problems and Solutions
Problem 1 (Batch LS and Covariance): Let \( \mathbf{Y}=\mathbf{\Phi}\boldsymbol{\theta}^{\star} +\mathbf{V} \), where \( \mathbb{E}[\mathbf{V}\mid\mathbf{\Phi}]=\mathbf{0} \) and \( \operatorname{Cov}(\mathbf{V}\mid\mathbf{\Phi}) =\sigma^{2}\mathbf{I} \). Derive the conditional mean and covariance of the LS estimate.
Solution:
\[ \widehat{\boldsymbol{\theta} } = (\mathbf{\Phi}^{T}\mathbf{\Phi})^{-1} \mathbf{\Phi}^{T} (\mathbf{\Phi}\boldsymbol{\theta}^{\star}+\mathbf{V}) = \boldsymbol{\theta}^{\star} + (\mathbf{\Phi}^{T}\mathbf{\Phi})^{-1} \mathbf{\Phi}^{T}\mathbf{V}. \]
Taking the conditional expectation gives
\[ \mathbb{E}[ \widehat{\boldsymbol{\theta} } \mid\mathbf{\Phi}] = \boldsymbol{\theta}^{\star}. \]
The covariance is
\[ \begin{aligned} \operatorname{Cov}( \widehat{\boldsymbol{\theta} }\mid\mathbf{\Phi}) &= (\mathbf{\Phi}^{T}\mathbf{\Phi})^{-1} \mathbf{\Phi}^{T} (\sigma^{2}\mathbf{I}) \mathbf{\Phi} (\mathbf{\Phi}^{T}\mathbf{\Phi})^{-1}\\ &= \sigma^{2} (\mathbf{\Phi}^{T}\mathbf{\Phi})^{-1}. \end{aligned} \]
Problem 2 (Certainty-Equivalent Tracking Error): For \( y(k+1)=\boldsymbol{\phi}^{T}(k) \boldsymbol{\theta}^{\star}+v(k+1) \), suppose the controller enforces \( \boldsymbol{\phi}^{T}(k)\widehat{\boldsymbol{\theta} }(k) =p_{d}y(k)+(1-p_{d})r(k) \). Derive the tracking-error equation relative to \( y_{m}(k+1)=p_{d}y_{m}(k)+(1-p_{d})r(k) \).
Solution: Write
\[ \boldsymbol{\theta}^{\star} = \widehat{\boldsymbol{\theta} }(k) + \widetilde{\boldsymbol{\theta} }(k). \]
Then
\[ y(k+1) = p_{d}y(k)+(1-p_{d})r(k) + \boldsymbol{\phi}^{T}(k) \widetilde{\boldsymbol{\theta} }(k) +v(k+1). \]
Subtracting the reference model yields
\[ e(k+1) = p_{d}e(k) + \boldsymbol{\phi}^{T}(k) \widetilde{\boldsymbol{\theta} }(k) +v(k+1). \]
Therefore the parameter error influences tracking only through its projection on the current regressor.
Problem 3 (Why Constant Regulation Can Destroy Identifiability): Suppose a two-parameter model has \( \boldsymbol{\phi}(k)=[y(k)\;\;u(k)]^{T} \). Under perfect constant-reference regulation, \( y(k)=\overline{y} \) and \( u(k)=\overline{u} \). Determine the rank of the finite-window information matrix.
Solution:
\[ \mathbf{R}_{L} = \sum_{i=k}^{k+L-1} \boldsymbol{\phi}(i)\boldsymbol{\phi}^{T}(i) = L \begin{bmatrix} \overline{y}\\\overline{u} \end{bmatrix} \begin{bmatrix} \overline{y}&\overline{u} \end{bmatrix}. \]
This is an outer product and therefore has rank at most one. A two-parameter vector cannot be uniquely identified. Good regulation has removed the independent variation needed for identification.
Problem 4 (Numerical Controller Update): Let \( \widehat{a}=0.70 \), \( \widehat{b}=0.50 \), \( p_{d}=0.20 \), \( y(k)=0.40 \), and \( r(k)=1 \). Compute the certainty-equivalent input.
Solution:
\[ \begin{aligned} u(k) &= \frac{ (0.20-0.70)(0.40)+(1-0.20)(1) }{0.50}\\ &= \frac{-0.20+0.80}{0.50} = 1.20. \end{aligned} \]
Problem 5 (Classification): Classify each task as primarily offline identification, online adaptive control, or a hybrid workflow: (a) estimating ten candidate model orders from a stored PRBS experiment; (b) updating a pole-placement controller every sample; (c) identifying a model offline, deploying it, and periodically re-identifying during scheduled maintenance; (d) injecting a small safe dither while a controller adapts.
Solution: (a) offline identification, because model selection and validation use a fixed record; (b) online adaptive control, because estimates immediately update feedback; (c) hybrid, because identification and deployment are separated but repeated over the asset life; (d) online adaptive control with deliberate excitation, because probing and regulation occur simultaneously.
Problem 6 (Control-Relevant Accuracy): Two models have equal unweighted prediction error. Model A is accurate near the closed-loop bandwidth but less accurate at very high frequency. Model B has the opposite error distribution. Which is generally more useful for feedback design, assuming unmodeled high-frequency dynamics remain adequately bounded?
Solution: Model A is generally more useful because errors near crossover and the closed-loop bandwidth strongly affect sensitivity, phase margin, and performance. The qualification is essential: high-frequency mismatch must still be represented conservatively enough to avoid robustness failure. Control relevance changes the weighting of model error; it does not permit arbitrary neglect of unmodeled dynamics.
19. Summary
Offline system identification and indirect adaptive control share regressors, prediction errors, least squares, and excitation concepts, but they differ in objective, timing, data generation, validation, computational constraints, and risk. Offline identification treats the validated model as the principal result. An STR treats online estimates as internal variables used to update feedback. The estimator and controller form a coupled nonlinear closed-loop system: feedback shapes the data, the data shape the estimate, and the estimate shapes future feedback. This coupling explains why parameter convergence may fail despite good tracking, why safe excitation is difficult, and why prediction accuracy alone is not a complete measure of control value.
20. References
- Åström, K.J., & Eykhoff, P. (1971). System identification—A survey. Automatica, 7(2), 123–162.
- Åström, K.J., & Wittenmark, B. (1973). On self-tuning regulators. Automatica, 9(2), 185–199.
- Åström, K.J., Borisson, U., Ljung, L., & Wittenmark, B. (1977). Theory and applications of self-tuning regulators. Automatica, 13(5), 457–476.
- Ljung, L. (1977). Analysis of recursive stochastic algorithms. IEEE Transactions on Automatic Control, 22(4), 551–575.
- Goodwin, G.C., Ramadge, P.J., & Caines, P.E. (1980). Discrete-time multivariable adaptive control. IEEE Transactions on Automatic Control, 25(3), 449–456.
- Hjalmarsson, H., Gevers, M., & de Bruyne, F. (1996). For model-based control design, closed-loop identification gives better performance. Automatica, 32(12), 1659–1673.
- Forssell, U., & Ljung, L. (1999). Closed-loop identification revisited. Automatica, 35(7), 1215–1241.
- Hjalmarsson, H. (2005). From experiment design to closed-loop control. Automatica, 41(3), 393–438.
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.