Chapter 16: Adaptive Control of Systems with Unknown High-Frequency Gain Sign
Lesson 1: Problem of Unknown Control Direction
This lesson formulates the unknown-control-direction problem and proves why conventional fixed-gain and Lyapunov-based adaptive controllers normally require prior knowledge of the sign of the plant input gain. The analysis begins with a scalar unstable plant, extends the interpretation to general SISO transfer functions through the high-frequency gain, and develops a counterexample in which a wrong sign assumption converts adaptive damping into rapidly increasing positive feedback.
1. Learning Objectives and Central Question
After completing this lesson, students should be able to:
- define control direction and the sign of the high-frequency gain;
- explain why unknown gain magnitude is easier than unknown gain sign;
- prove that one fixed linear feedback gain cannot stabilize both possible input directions;
- derive the sign-dependent term in a standard MRAC update law;
- analyze a minimal adaptive counterexample with a wrong sign assumption; and
- state the assumptions under which later unknown-direction methods are meaningful.
The central issue is not merely that the coefficient multiplying the control input is uncertain. The difficulty is that its sign determines whether a commanded input pushes the state in the intended direction or in exactly the opposite direction. A controller that increases its gain in response to a large error can therefore either increase damping or amplify the instability.
flowchart TD
A["Tracking or regulation error is large"] --> B["Controller increases corrective action"]
B --> C{"Does assumed input \ndirection match plant?"}
C -->|"yes"| D["Negative feedback becomes stronger"]
D --> E["Error decreases and adaptation slows"]
C -->|"no"| F["Loop acts as positive feedback"]
F --> G["Error and adaptive gain reinforce each other"]
G --> H["Runaway transient or instability"]
2. Control Direction in a Scalar Plant
Consider the continuous-time scalar plant
\[ \dot{x}(t)=a x(t)+b u(t), \qquad b\neq 0. \]
The sign of \(b\) is the control direction. For an infinitesimal positive input increment \(\delta u>0\), the instantaneous state-derivative increment is
\[ \delta \dot{x}=b\,\delta u. \]
Hence \(b>0\) means that positive input initially increases \(\dot{x}\), whereas \(b<0\) means that the same input decreases it. If the magnitude \(|b|\) is unknown but the sign is known, adaptation can scale a correctly oriented control action. If the sign itself is unknown, the controller does not initially know which orientation is corrective.
For the static feedback \(u=-k x\), the closed-loop dynamics are
\[ \dot{x}=(a-bk)x. \]
The scalar closed loop is exponentially stable precisely when
\[ a-bk<0. \]
If \(b>0\), sufficiently large positive \(k\) stabilizes the plant. If \(b<0\), the same positive gain makes the closed-loop pole move to the right; a stabilizing gain must instead have the opposite sign.
3. High-Frequency Gain for General SISO Systems
For a proper SISO transfer function with relative degree \(\rho\), write
\[ G(s)=\frac{Y(s)}{U(s)}=k_p\frac{Z(s)}{P(s)}, \]
where \(Z(s)\) and \(P(s)\) are monic and \(\deg P-\deg Z=\rho\). The high-frequency gain is
\[ k_{\mathrm{hf} }=\lim_{s\to\infty}s^{\rho}G(s)=k_p. \]
Its sign determines the leading input-to-output orientation after the known relative-degree dynamics are accounted for. In a controllable canonical realization of relative degree \(\rho\), the same quantity appears as the coefficient multiplying the input in the highest output derivative:
\[ y^{(\rho)}=\text{known and uncertain state terms}+k_{\mathrm{hf} }u. \]
Therefore, “unknown control direction” and “unknown high-frequency gain sign” describe the same structural obstacle for the SISO classes studied in this chapter. The magnitude may be uncertain over a continuum, but the sign uncertainty separates the plant family into two disconnected cases.
Example. For
\[ G(s)=\frac{-3s+6}{s^2+4s+5}, \]
the relative degree is one and \(k_{\mathrm{hf} }=-3\). A controller designed under a positive high-frequency-gain assumption applies the high-bandwidth action in the wrong direction.
4. Fixed-Feedback Impossibility Result
Theorem 1 (two-direction fixed-gain obstruction).
Let \(a\ge 0\) and \(\beta>0\). Consider the two plants
\[ \dot{x}=a x+\beta u, \qquad \dot{x}=a x-\beta u. \]
No single constant linear feedback \(u=-kx\) locally exponentially stabilizes both plants.
Proof. Their closed-loop poles are
\[ \lambda_{+}=a-\beta k, \qquad \lambda_{-}=a+\beta k. \]
Both would have to be negative. However,
\[ \lambda_{+}+\lambda_{-}=2a\ge 0, \]
which contradicts the requirement that both terms be negative. Therefore no such \(k\) exists. \(\square\)
Corollary 1 (local smooth static feedback).
Let \(u=\alpha(x)\), where \(\alpha(0)=0\) and \(\alpha\) is differentiable at the origin. The two linearizations have poles
\[ \lambda_{+}=a+\beta\alpha'(0), \qquad \lambda_{-}=a-\beta\alpha'(0). \]
Their sum is again \(2a\ge0\). Thus one smooth static law cannot provide local exponential stabilization for both directions. A successful universal design must introduce additional dynamics, switching, probing, or a specially structured time-varying gain.
5. Where the Sign Appears in Standard MRAC
Consider the plant and stable reference model
\[ \dot{x}=a x+b u, \qquad \dot{x}_m=a_m x_m+b_m r, \qquad a_m<0. \]
Use the direct adaptive control law
\[ u=\hat{\theta}_x x+\hat{\theta}_r r =\hat{\boldsymbol{\theta} }^{T}\boldsymbol{\phi}, \qquad \boldsymbol{\phi}=\begin{bmatrix}x & r\end{bmatrix}^{T}. \]
The matching or ideal parameters satisfy
\[ a+b\theta_x^{*}=a_m, \qquad b\theta_r^{*}=b_m, \]
\[ \theta_x^{*}=\frac{a_m-a}{b}, \qquad \theta_r^{*}=\frac{b_m}{b}. \]
Define the tracking error \(e=x-x_m\) and parameter error \(\tilde{\boldsymbol{\theta} }= \hat{\boldsymbol{\theta} }-\boldsymbol{\theta}^{*}\). Then
\[ \dot{e}=a_m e+b\tilde{\boldsymbol{\theta} }^{T}\boldsymbol{\phi}. \]
Choose the Lyapunov candidate
\[ V=\frac{1}{2}e^2+ \frac{|b|}{2\gamma}\tilde{\boldsymbol{\theta} }^{T} \tilde{\boldsymbol{\theta} }, \qquad \gamma>0. \]
Its derivative is
\[ \dot{V}=a_m e^2+b e\tilde{\boldsymbol{\theta} }^{T}\boldsymbol{\phi} +\frac{|b|}{\gamma}\tilde{\boldsymbol{\theta} }^{T} \dot{\hat{\boldsymbol{\theta} } }. \]
Exact cancellation of the cross term requires the update law
\[ \dot{\hat{\boldsymbol{\theta} } } =-\gamma\operatorname{sgn}(b)\boldsymbol{\phi}e. \]
Indeed, substitution gives
\[ \dot{V}=a_m e^2\le0. \]
This familiar proof does not require the magnitude of \(b\) in the implementable update law, but it does require \(\operatorname{sgn}(b)\). If the controller uses an assumed sign \(\hat{\sigma}\), then
\[ \dot{\hat{\boldsymbol{\theta} } } =-\gamma\hat{\sigma}\boldsymbol{\phi}e \]
produces
\[ \dot{V}=a_m e^2+ \left(b-|b|\hat{\sigma}\right) e\tilde{\boldsymbol{\theta} }^{T}\boldsymbol{\phi}. \]
For the correct sign, the second term vanishes. For the opposite sign, \(\hat{\sigma}=-\operatorname{sgn}(b)\), it becomes
\[ \dot{V}=a_m e^2+2b e \tilde{\boldsymbol{\theta} }^{T}\boldsymbol{\phi}, \]
which has no definite sign. The standard Lyapunov argument therefore fails for a structural reason, not because of an algebraic inconvenience.
6. Minimal Adaptive High-Gain Counterexample
Consider the unstable scalar plant
\[ \dot{x}=a x+b u, \qquad a>0, \quad b\neq0, \]
with the sign-dependent adaptive high-gain controller
\[ u=-\hat{\sigma}k x, \qquad \dot{k}=\gamma x^2, \qquad k(0)=k_0\ge0. \]
Define
\[ q=b\hat{\sigma}. \]
The closed-loop equations become
\[ \dot{x}=(a-qk)x, \qquad \dot{k}=\gamma x^2. \]
6.1 Correct Sign Assumption
If \(q>0\), choose any constant \(k^{*}>a/q\) and define
\[ V=\frac{1}{2}x^2+\frac{q}{2\gamma}(k-k^{*})^2. \]
Then
\[ \begin{aligned} \dot{V} &=x(a-qk)x+\frac{q}{\gamma}(k-k^{*})\gamma x^2\\ &=\left(a-qk^{*}\right)x^2\\ &=-\left(qk^{*}-a\right)x^2\le0. \end{aligned} \]
Thus \(x\) and \(k\) are bounded and \(x\in L_2\). Since the closed-loop derivatives are bounded, Barbalat's lemma yields \(x(t)\to0\). Notice that the gain need not converge to a unique plant parameter; it only reaches a sufficient stabilizing level.
6.2 Wrong Sign Assumption
If \(q<0\), let \(c=-q>0\). Then
\[ \dot{x}=(a+ck)x, \qquad \dot{k}=\gamma x^2. \]
The adaptive gain is nondecreasing, and every gain increase strengthens the unstable term. Eliminating time gives an exact trajectory relation. Since
\[ \frac{d(x^2)}{dt}=2(a+ck)x^2, \qquad \frac{dk}{dt}=\gamma x^2, \]
we obtain
\[ \frac{d(x^2)}{dk}=\frac{2}{\gamma}(a+ck). \]
Integration from \((x_0,k_0)\) gives
\[ x^2=x_0^2+\frac{2a}{\gamma}(k-k_0) +\frac{c}{\gamma}(k^2-k_0^2). \]
Consequently,
\[ \dot{k}=\gamma x_0^2+2a(k-k_0)+c(k^2-k_0^2). \]
For sufficiently large \(k\), the quadratic term dominates, so there exists \(K>0\) such that
\[ \dot{k}\ge\frac{c}{2}k^2 \qquad \text{for } k\ge K. \]
Comparison with \(\dot{z}=(c/2)z^2\) proves finite escape. Therefore, the wrong sign can produce more than slow convergence or a large transient: in this idealized model it can generate a finite-time singularity. Numerical implementations must stop before overflow.
7. Why Online Identification Does Not Immediately Remove the Difficulty
If \(a\) were known and exact derivatives were available, one could formally compute
\[ b=\frac{\dot{x}-ax}{u}, \qquad u\neq0. \]
In practice, this observation does not by itself solve the stabilization problem:
- when \(u=0\), the sign is unidentifiable;
- differentiating measured states amplifies noise;
- if \(a\) is also unknown, the regression must separate correlated terms;
- persistent excitation is needed for reliable parameter convergence; and
- the plant may diverge before an indirect estimator establishes the correct sign.
The controller must therefore balance information acquisition and stabilization. This is a dual-control-like tension: an input useful for identifying the direction may temporarily excite an unstable plant, while a conservative input may provide insufficient information.
flowchart TD
A["Input-gain uncertainty"] --> B{"Which uncertainty \nclass?"}
B --> C["Magnitude unknown, \nsign known"]
C --> D["Standard gradient or \nLyapunov adaptation \nis usually applicable"]
B --> E["Constant sign unknown"]
E --> F["Special dynamic gain,\n switching, or \nprobing is required"]
B --> G["Time-varying but \nsign-preserving gain"]
G --> H["Additional rate and \nboundedness assumptions \nare required"]
B --> I["Gain crosses zero \nor changes sign"]
I --> J["Classical unknown-direction \ntheory may no longer apply"]
8. Well-Posed Assumptions for the Rest of Chapter 16
Later lessons will study controllers that do not require prior knowledge of the sign. Their guarantees still depend on structural assumptions. A representative SISO problem statement is:
\[ \dot{x}=f(x,t)+b(t)u, \]
with the following requirements:
- Nonvanishing authority: \(|b(t)|\ge b_{\min}>0\).
- Fixed direction: the sign of \(b(t)\) is constant, although unknown.
- Known input location: the channel through which \(u\) enters is known.
- Known relative degree: the input-output differentiation structure is known.
- Compatible internal dynamics: unforced internal dynamics satisfy the stability assumptions required by the chosen design.
- Implementable signals: the variables used in the update law are measured or generated by stable filters.
A gain that can cross zero creates intervals with vanishing control authority and reverses the feedback orientation. That problem is fundamentally harder and is not covered by the classical constant unknown-direction setting.
9. Computational Experiment
The implementations below simulate
\[ \dot{x}=0.8x+bu, \qquad u=-\hat{\sigma}kx, \qquad \dot{k}=x^2, \]
for four cases:
- \(b=+1.5\), \(\hat{\sigma}=+1\);
- \(b=-1.5\), \(\hat{\sigma}=-1\);
- \(b=+1.5\), \(\hat{\sigma}=-1\); and
- \(b=-1.5\), \(\hat{\sigma}=+1\).
The initial conditions are \(x(0)=1\) and \(k(0)=0.1\). A fixed-step fourth-order Runge-Kutta method uses \(\Delta t=10^{-4}\). The computation stops if \(|x|\) or \(|k|\) reaches \(10^3\), because the wrong-sign ideal model has finite escape and should not be integrated through numerical overflow.
With the correct sign, both positive- and negative- \(b\) cases are dynamically identical after multiplication by \(\hat{\sigma}\). With the wrong sign, the two cases are likewise identical and trigger the safety stop near \(t=0.883\) for the selected parameters.
9.1 Implementation Ecosystem
| Environment | Libraries or facilities | Role in this lesson |
|---|---|---|
| Python |
NumPy, Matplotlib; optionally
SciPy or python-control
|
Vector arithmetic, plotting, ODE solvers, and linear-system utilities. The supplied RK4 solver is implemented from scratch. |
| C++ | C++17 standard library; optionally Eigen |
Portable real-time-oriented implementation. Eigen becomes useful when scalar gains are replaced by parameter vectors and matrices. |
| Java | Java 17 standard library; optionally Apache Commons Math | Structured simulation, file output, and optional access to general numerical integrators and linear algebra. |
| MATLAB/Simulink | MATLAB numerical functions, Control System Toolbox, Simulink | Rapid control analysis, block-diagram realization, solver selection, logging, and hardware-oriented model development. |
| Wolfram Mathematica |
NDSolveValue, WhenEvent, symbolic
algebra, plotting
|
Symbolic verification of trajectory relations and event-driven numerical integration. |
General-purpose control packages do not usually provide a single
built-in command for unknown-direction adaptive control. The update laws
are normally assembled explicitly from plant equations, filters,
integrators, and safety logic. The downloadable bundle is named
Chapter16_Lesson1_Code.zip.
10. Python Implementation
This implementation uses NumPy for state vectors and
Matplotlib for logarithmic response plots. The RK4
integrator and safety event are implemented explicitly.
Chapter16_Lesson1.py
"""Chapter16_Lesson1.py
Numerical demonstration of the unknown-control-direction problem.
Plant:
x_dot = a*x + b*u
Controller (deliberately sign-dependent):
u = -sigma_hat*k*x
k_dot = gamma*x^2
When sigma_hat has the same sign as b, the adaptive high-gain law stabilizes
the scalar plant. When the assumed sign is wrong, the loop becomes positive
feedback and the safety threshold is reached rapidly.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import csv
import matplotlib.pyplot as plt
import numpy as np
@dataclass(frozen=True)
class Case:
name: str
b: float
sigma_hat: float
A = 0.8
GAMMA = 1.0
DT = 1.0e-4
T_FINAL = 8.0
X0 = 1.0
K0 = 0.1
SAFETY_LIMIT = 1.0e3
def rhs(state: np.ndarray, b: float, sigma_hat: float) -> np.ndarray:
"""Return [x_dot, k_dot]."""
x, k = state
u = -sigma_hat * k * x
return np.array([A * x + b * u, GAMMA * x * x], dtype=float)
def rk4_step(state: np.ndarray, b: float, sigma_hat: float) -> np.ndarray:
"""One fixed-step classical Runge-Kutta update."""
k1 = rhs(state, b, sigma_hat)
k2 = rhs(state + 0.5 * DT * k1, b, sigma_hat)
k3 = rhs(state + 0.5 * DT * k2, b, sigma_hat)
k4 = rhs(state + DT * k3, b, sigma_hat)
return state + (DT / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)
def simulate(case: Case) -> tuple[np.ndarray, np.ndarray, np.ndarray, bool]:
state = np.array([X0, K0], dtype=float)
time = 0.0
times = [time]
xs = [state[0]]
ks = [state[1]]
tripped = False
max_steps = int(round(T_FINAL / DT))
for _ in range(max_steps):
candidate = rk4_step(state, case.b, case.sigma_hat)
time += DT
if (
not np.all(np.isfinite(candidate))
or abs(candidate[0]) >= SAFETY_LIMIT
or abs(candidate[1]) >= SAFETY_LIMIT
):
tripped = True
break
state = candidate
times.append(time)
xs.append(state[0])
ks.append(state[1])
return np.asarray(times), np.asarray(xs), np.asarray(ks), tripped
def write_csv(path: Path, t: np.ndarray, x: np.ndarray, k: np.ndarray) -> None:
with path.open("w", newline="", encoding="utf-8") as stream:
writer = csv.writer(stream)
writer.writerow(["time", "x", "k"])
writer.writerows(zip(t, x, k))
def main() -> None:
cases = [
Case("b_pos_assumption_pos", b=+1.5, sigma_hat=+1.0),
Case("b_neg_assumption_neg", b=-1.5, sigma_hat=-1.0),
Case("b_pos_assumption_neg", b=+1.5, sigma_hat=-1.0),
Case("b_neg_assumption_pos", b=-1.5, sigma_hat=+1.0),
]
output_dir = Path("Chapter16_Lesson1_output")
output_dir.mkdir(exist_ok=True)
results: dict[str, tuple[np.ndarray, np.ndarray, np.ndarray, bool]] = {}
for case in cases:
result = simulate(case)
results[case.name] = result
t, x, k, tripped = result
write_csv(output_dir / f"{case.name}.csv", t, x, k)
relation = "correct" if case.b * case.sigma_hat > 0.0 else "wrong"
status = "SAFETY STOP" if tripped else "completed"
print(
f"{case.name:24s} | sign={relation:7s} | {status:11s} | "
f"t={t[-1]:.4f}, x={x[-1]:.6g}, k={k[-1]:.6g}"
)
plt.figure()
for case in cases:
t, x, _, _ = results[case.name]
plt.semilogy(t, np.maximum(np.abs(x), 1.0e-12), label=case.name)
plt.xlabel("time")
plt.ylabel("absolute state |x|")
plt.title("Unknown control direction: state response")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.savefig(output_dir / "state_response.png", dpi=180)
plt.figure()
for case in cases:
t, _, k, _ = results[case.name]
plt.plot(t, k, label=case.name)
plt.xlabel("time")
plt.ylabel("adaptive gain k")
plt.title("Unknown control direction: gain evolution")
plt.yscale("log")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.savefig(output_dir / "gain_response.png", dpi=180)
plt.show()
if __name__ == "__main__":
main()
11. C++ Implementation
The C++17 program uses only the standard library. It writes one CSV file per sign configuration and demonstrates a portable from-scratch numerical implementation.
Chapter16_Lesson1.cpp
// Chapter16_Lesson1.cpp
// Unknown-control-direction demonstration using a from-scratch RK4 solver.
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <string>
#include <vector>
struct State {
double x;
double k;
};
struct CaseData {
std::string name;
double b;
double sigma_hat;
};
struct Sample {
double time;
State state;
};
constexpr double A = 0.8;
constexpr double GAMMA = 1.0;
constexpr double DT = 1.0e-4;
constexpr double T_FINAL = 8.0;
constexpr double X0 = 1.0;
constexpr double K0 = 0.1;
constexpr double SAFETY_LIMIT = 1.0e3;
State add(const State& lhs, const State& rhs, double scale) {
return {lhs.x + scale * rhs.x, lhs.k + scale * rhs.k};
}
State rhs(const State& state, double b, double sigma_hat) {
const double u = -sigma_hat * state.k * state.x;
return {A * state.x + b * u, GAMMA * state.x * state.x};
}
State rk4Step(const State& state, double b, double sigma_hat) {
const State k1 = rhs(state, b, sigma_hat);
const State k2 = rhs(add(state, k1, 0.5 * DT), b, sigma_hat);
const State k3 = rhs(add(state, k2, 0.5 * DT), b, sigma_hat);
const State k4 = rhs(add(state, k3, DT), b, sigma_hat);
return {
state.x + (DT / 6.0) * (k1.x + 2.0 * k2.x + 2.0 * k3.x + k4.x),
state.k + (DT / 6.0) * (k1.k + 2.0 * k2.k + 2.0 * k3.k + k4.k)
};
}
bool safe(const State& state) {
return std::isfinite(state.x) && std::isfinite(state.k)
&& std::abs(state.x) < SAFETY_LIMIT
&& std::abs(state.k) < SAFETY_LIMIT;
}
std::pair<std::vector<Sample>, bool> simulate(const CaseData& test_case) {
std::vector<Sample> samples;
samples.reserve(static_cast<std::size_t>(T_FINAL / DT) + 1U);
State state{X0, K0};
double time = 0.0;
samples.push_back({time, state});
bool tripped = false;
const int max_steps = static_cast<int>(std::llround(T_FINAL / DT));
for (int step = 0; step < max_steps; ++step) {
const State candidate = rk4Step(state, test_case.b, test_case.sigma_hat);
time += DT;
if (!safe(candidate)) {
tripped = true;
break;
}
state = candidate;
samples.push_back({time, state});
}
return {samples, tripped};
}
void writeCsv(const std::filesystem::path& path, const std::vector<Sample>& samples) {
std::ofstream stream(path);
if (!stream) {
throw std::runtime_error("Cannot open output file: " + path.string());
}
stream << "time,x,k\n";
stream << std::setprecision(12);
for (const Sample& sample : samples) {
stream << sample.time << ',' << sample.state.x << ',' << sample.state.k << '\n';
}
}
int main() {
const std::vector<CaseData> cases{
{"b_pos_assumption_pos", +1.5, +1.0},
{"b_neg_assumption_neg", -1.5, -1.0},
{"b_pos_assumption_neg", +1.5, -1.0},
{"b_neg_assumption_pos", -1.5, +1.0}
};
const std::filesystem::path output_dir{"Chapter16_Lesson1_output_cpp"};
std::filesystem::create_directories(output_dir);
try {
for (const CaseData& test_case : cases) {
const auto [samples, tripped] = simulate(test_case);
writeCsv(output_dir / (test_case.name + ".csv"), samples);
const Sample& last = samples.back();
const bool correct_sign = test_case.b * test_case.sigma_hat > 0.0;
std::cout << std::left << std::setw(24) << test_case.name
<< " | sign=" << std::setw(7) << (correct_sign ? "correct" : "wrong")
<< " | " << std::setw(11) << (tripped ? "SAFETY STOP" : "completed")
<< " | t=" << std::fixed << std::setprecision(4) << last.time
<< ", x=" << std::setprecision(6) << last.state.x
<< ", k=" << last.state.k << '\n';
}
} catch (const std::exception& error) {
std::cerr << "Error: " << error.what() << '\n';
return 1;
}
return 0;
}
12. Java Implementation
The Java 17 implementation uses records for immutable simulation data, performs explicit RK4 integration, and writes CSV outputs through the standard NIO API.
Chapter16_Lesson1.java
// Chapter16_Lesson1.java
// Unknown-control-direction demonstration using a from-scratch RK4 solver.
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public final class Chapter16_Lesson1 {
private static final double A = 0.8;
private static final double GAMMA = 1.0;
private static final double DT = 1.0e-4;
private static final double T_FINAL = 8.0;
private static final double X0 = 1.0;
private static final double K0 = 0.1;
private static final double SAFETY_LIMIT = 1.0e3;
private Chapter16_Lesson1() {
}
private record State(double x, double k) {
}
private record TestCase(String name, double b, double sigmaHat) {
}
private record Sample(double time, State state) {
}
private record Result(List<Sample> samples, boolean tripped) {
}
private static State add(State lhs, State rhs, double scale) {
return new State(lhs.x() + scale * rhs.x(), lhs.k() + scale * rhs.k());
}
private static State rhs(State state, double b, double sigmaHat) {
double u = -sigmaHat * state.k() * state.x();
return new State(A * state.x() + b * u, GAMMA * state.x() * state.x());
}
private static State rk4Step(State state, double b, double sigmaHat) {
State k1 = rhs(state, b, sigmaHat);
State k2 = rhs(add(state, k1, 0.5 * DT), b, sigmaHat);
State k3 = rhs(add(state, k2, 0.5 * DT), b, sigmaHat);
State k4 = rhs(add(state, k3, DT), b, sigmaHat);
return new State(
state.x() + (DT / 6.0) * (k1.x() + 2.0 * k2.x() + 2.0 * k3.x() + k4.x()),
state.k() + (DT / 6.0) * (k1.k() + 2.0 * k2.k() + 2.0 * k3.k() + k4.k())
);
}
private static boolean safe(State state) {
return Double.isFinite(state.x())
&& Double.isFinite(state.k())
&& Math.abs(state.x()) < SAFETY_LIMIT
&& Math.abs(state.k()) < SAFETY_LIMIT;
}
private static Result simulate(TestCase testCase) {
int maxSteps = (int) Math.round(T_FINAL / DT);
List<Sample> samples = new ArrayList<>(maxSteps + 1);
State state = new State(X0, K0);
double time = 0.0;
samples.add(new Sample(time, state));
boolean tripped = false;
for (int step = 0; step < maxSteps; step++) {
State candidate = rk4Step(state, testCase.b(), testCase.sigmaHat());
time += DT;
if (!safe(candidate)) {
tripped = true;
break;
}
state = candidate;
samples.add(new Sample(time, state));
}
return new Result(samples, tripped);
}
private static void writeCsv(Path path, List<Sample> samples) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
writer.write("time,x,k");
writer.newLine();
for (Sample sample : samples) {
writer.write(String.format(
Locale.US,
"%.12g,%.12g,%.12g",
sample.time(), sample.state().x(), sample.state().k()
));
writer.newLine();
}
}
}
public static void main(String[] args) {
Locale.setDefault(Locale.US);
List<TestCase> cases = List.of(
new TestCase("b_pos_assumption_pos", +1.5, +1.0),
new TestCase("b_neg_assumption_neg", -1.5, -1.0),
new TestCase("b_pos_assumption_neg", +1.5, -1.0),
new TestCase("b_neg_assumption_pos", -1.5, +1.0)
);
Path outputDir = Paths.get("Chapter16_Lesson1_output_java");
try {
Files.createDirectories(outputDir);
for (TestCase testCase : cases) {
Result result = simulate(testCase);
writeCsv(outputDir.resolve(testCase.name() + ".csv"), result.samples());
Sample last = result.samples().get(result.samples().size() - 1);
boolean correctSign = testCase.b() * testCase.sigmaHat() > 0.0;
System.out.printf(
"%-24s | sign=%-7s | %-11s | t=%.4f, x=%.6g, k=%.6g%n",
testCase.name(),
correctSign ? "correct" : "wrong",
result.tripped() ? "SAFETY STOP" : "completed",
last.time(), last.state().x(), last.state().k()
);
}
} catch (IOException error) {
System.err.println("I/O error: " + error.getMessage());
System.exit(1);
}
}
}
13. MATLAB and Simulink Implementation
The MATLAB script performs the same fixed-step simulation, exports plots and tables, and programmatically creates a Simulink model when a Simulink license is available. The generated block diagram contains two integrator states, \(x\) and \(k\), and directly realizes the plant, controller, and adaptation equations.
Chapter16_Lesson1.m
%% Chapter16_Lesson1.m
% Unknown-control-direction demonstration using a fixed-step RK4 solver.
%
% Plant: x_dot = a*x + b*u
% Controller: u = -sigma_hat*k*x, k_dot = gamma*x^2
%
% The script also creates a Simulink realization when Simulink is licensed.
clear; clc; close all;
a = 0.8;
gamma = 1.0;
dt = 1.0e-4;
tFinal = 8.0;
x0 = 1.0;
k0 = 0.1;
safetyLimit = 1.0e3;
cases = struct( ...
'name', {'b_pos_assumption_pos', 'b_neg_assumption_neg', ...
'b_pos_assumption_neg', 'b_neg_assumption_pos'}, ...
'b', {+1.5, -1.5, +1.5, -1.5}, ...
'sigmaHat', {+1.0, -1.0, -1.0, +1.0});
outputDir = 'Chapter16_Lesson1_output_matlab';
if ~exist(outputDir, 'dir')
mkdir(outputDir);
end
results = cell(size(cases));
for index = 1:numel(cases)
results{index} = simulateCase(cases(index), a, gamma, dt, tFinal, ...
x0, k0, safetyLimit);
data = table(results{index}.time, results{index}.x, results{index}.k, ...
'VariableNames', {'time', 'x', 'k'});
writetable(data, fullfile(outputDir, [cases(index).name '.csv']));
if cases(index).b * cases(index).sigmaHat > 0
relation = 'correct';
else
relation = 'wrong';
end
if results{index}.tripped
status = 'SAFETY STOP';
else
status = 'completed';
end
fprintf('%-24s | sign=%-7s | %-11s | t=%.4f, x=%.6g, k=%.6g\n', ...
cases(index).name, relation, status, results{index}.time(end), ...
results{index}.x(end), results{index}.k(end));
end
figure;
hold on;
for index = 1:numel(cases)
semilogy(results{index}.time, max(abs(results{index}.x), 1.0e-12), ...
'DisplayName', cases(index).name);
end
hold off;
grid on;
xlabel('time');
ylabel('absolute state |x|');
title('Unknown control direction: state response');
legend('Location', 'best');
exportgraphics(gcf, fullfile(outputDir, 'state_response.png'), 'Resolution', 180);
figure;
hold on;
for index = 1:numel(cases)
semilogy(results{index}.time, results{index}.k, ...
'DisplayName', cases(index).name);
end
hold off;
grid on;
xlabel('time');
ylabel('adaptive gain k');
title('Unknown control direction: gain evolution');
legend('Location', 'best');
exportgraphics(gcf, fullfile(outputDir, 'gain_response.png'), 'Resolution', 180);
% Build one Simulink model for the b > 0, correct-sign case.
if license('test', 'Simulink')
buildSimulinkModel('Chapter16_Lesson1_Simulink', a, gamma, +1.5, +1.0, x0, k0);
fprintf('Created Chapter16_Lesson1_Simulink.slx\n');
else
fprintf('Simulink license not found; skipped model generation.\n');
end
function result = simulateCase(testCase, a, gamma, dt, tFinal, x0, k0, safetyLimit)
maxSteps = round(tFinal / dt);
time = zeros(maxSteps + 1, 1);
x = zeros(maxSteps + 1, 1);
k = zeros(maxSteps + 1, 1);
x(1) = x0;
k(1) = k0;
last = 1;
tripped = false;
for step = 1:maxSteps
state = [x(last); k(last)];
candidate = rk4Step(state, testCase.b, testCase.sigmaHat, a, gamma, dt);
if any(~isfinite(candidate)) || any(abs(candidate) >= safetyLimit)
tripped = true;
break;
end
last = last + 1;
time(last) = time(last - 1) + dt;
x(last) = candidate(1);
k(last) = candidate(2);
end
result.time = time(1:last);
result.x = x(1:last);
result.k = k(1:last);
result.tripped = tripped;
end
function next = rk4Step(state, b, sigmaHat, a, gamma, dt)
dynamics = @(z) [a*z(1) + b*(-sigmaHat*z(2)*z(1)); gamma*z(1)^2];
k1 = dynamics(state);
k2 = dynamics(state + 0.5*dt*k1);
k3 = dynamics(state + 0.5*dt*k2);
k4 = dynamics(state + dt*k3);
next = state + (dt/6)*(k1 + 2*k2 + 2*k3 + k4);
end
function buildSimulinkModel(modelName, a, gamma, b, sigmaHat, x0, k0)
if bdIsLoaded(modelName)
close_system(modelName, 0);
end
if exist([modelName '.slx'], 'file')
delete([modelName '.slx']);
end
new_system(modelName);
add_block('simulink/Continuous/Integrator', [modelName '/x'], ...
'InitialCondition', num2str(x0), 'Position', [500 90 530 120]);
add_block('simulink/Continuous/Integrator', [modelName '/k'], ...
'InitialCondition', num2str(k0), 'Position', [500 240 530 270]);
add_block('simulink/Math Operations/Gain', [modelName '/a'], ...
'Gain', num2str(a), 'Position', [180 65 240 95]);
add_block('simulink/Math Operations/Product', [modelName '/k_times_x'], ...
'Position', [170 130 215 170]);
add_block('simulink/Math Operations/Gain', [modelName '/minus_sigma'], ...
'Gain', num2str(-sigmaHat), 'Position', [255 130 315 170]);
add_block('simulink/Math Operations/Gain', [modelName '/b'], ...
'Gain', num2str(b), 'Position', [345 130 405 170]);
add_block('simulink/Math Operations/Sum', [modelName '/sum_xdot'], ...
'Inputs', '++', 'Position', [440 85 460 145]);
add_block('simulink/Math Operations/Math Function', [modelName '/x_squared'], ...
'Operator', 'square', 'Position', [190 225 250 265]);
add_block('simulink/Math Operations/Gain', [modelName '/gamma'], ...
'Gain', num2str(gamma), 'Position', [300 225 360 265]);
add_block('simulink/Signal Routing/Mux', [modelName '/Mux'], ...
'Inputs', '2', 'Position', [600 135 605 225]);
add_block('simulink/Sinks/To Workspace', [modelName '/To Workspace'], ...
'VariableName', 'sim_xk', 'SaveFormat', 'StructureWithTime', ...
'Position', [650 165 745 195]);
add_line(modelName, 'x/1', 'a/1');
add_line(modelName, 'a/1', 'sum_xdot/1');
add_line(modelName, 'x/1', 'k_times_x/1');
add_line(modelName, 'k/1', 'k_times_x/2');
add_line(modelName, 'k_times_x/1', 'minus_sigma/1');
add_line(modelName, 'minus_sigma/1', 'b/1');
add_line(modelName, 'b/1', 'sum_xdot/2');
add_line(modelName, 'sum_xdot/1', 'x/1');
add_line(modelName, 'x/1', 'x_squared/1');
add_line(modelName, 'x_squared/1', 'gamma/1');
add_line(modelName, 'gamma/1', 'k/1');
add_line(modelName, 'x/1', 'Mux/1');
add_line(modelName, 'k/1', 'Mux/2');
add_line(modelName, 'Mux/1', 'To Workspace/1');
set_param(modelName, 'StopTime', '8', 'Solver', 'ode45');
save_system(modelName);
close_system(modelName);
end
14. Wolfram Mathematica Implementation
The notebook uses NDSolveValue with an event that
terminates integration at the safety boundary. It exports case data and
two response plots.
Chapter16_Lesson1.nb
Notebook[{
Cell["Chapter 16, Lesson 1: Problem of Unknown Control Direction", "Title"],
Cell["The notebook compares correct and incorrect assumptions about the sign of the plant input gain.", "Text"],
Cell[BoxData[ToBoxes[Defer[
ClearAll["Global`*"];
a = 0.8;
gamma = 1.0;
tFinal = 8.0;
x0 = 1.0;
k0 = 0.1;
safetyLimit = 1000.0;
cases = {
<|"Name" -> "b_pos_assumption_pos", "b" -> 1.5, "SigmaHat" -> 1.0|>,
<|"Name" -> "b_neg_assumption_neg", "b" -> -1.5, "SigmaHat" -> -1.0|>,
<|"Name" -> "b_pos_assumption_neg", "b" -> 1.5, "SigmaHat" -> -1.0|>,
<|"Name" -> "b_neg_assumption_pos", "b" -> -1.5, "SigmaHat" -> 1.0|>
};
simulate[case_] := Module[
{b, sigmaHat, equations, event, solution, endTime, data},
b = case["b"];
sigmaHat = case["SigmaHat"];
equations = {
x'[t] == a*x[t] + b*(-sigmaHat*k[t]*x[t]),
k'[t] == gamma*x[t]^2,
x[0] == x0,
k[0] == k0
};
event = WhenEvent[
Abs[x[t]] >= safetyLimit || Abs[k[t]] >= safetyLimit,
"StopIntegration"
];
solution = NDSolveValue[
Join[equations, {event}], {x, k}, {t, 0, tFinal},
Method -> {"EventLocator", "EventAction" -> "StopIntegration"},
MaxStepFraction -> 1/10000
];
endTime = Min[tFinal, solution[[1]]["Domain"][[1, 2]]];
data = Table[
{time, solution[[1]][time], solution[[2]][time]},
{time, 0, endTime, Max[endTime/2000, 0.0001]}
];
<|"Case" -> case, "EndTime" -> endTime, "Data" -> data|>
];
results = simulate /@ cases;
summaries = Table[
With[
{case = result["Case"], last = Last[result["Data"]]},
<|
"Name" -> case["Name"],
"SignRelation" -> If[case["b"]*case["SigmaHat"] > 0, "correct", "wrong"],
"EndTime" -> last[[1]],
"FinalX" -> last[[2]],
"FinalK" -> last[[3]]
|>
],
{result, results}
];
Dataset[summaries];
statePlot = ListLinePlot[
Table[Map[{ #[[1]], Max[Abs[#[[2]]], 10^-12] } &, result["Data"]], {result, results}],
PlotLegends -> Lookup[cases, "Name"],
Frame -> True,
FrameLabel -> {"time", "absolute state |x|"},
PlotLabel -> "Unknown control direction: state response",
ScalingFunctions -> {None, "Log"},
ImageSize -> Large
];
gainPlot = ListLogPlot[
Table[result["Data"][[All, {1, 3}]], {result, results}],
PlotLegends -> Lookup[cases, "Name"],
Frame -> True,
FrameLabel -> {"time", "adaptive gain k"},
PlotLabel -> "Unknown control direction: gain evolution",
ImageSize -> Large
];
outputDirectory = FileNameJoin[{NotebookDirectory[], "Chapter16_Lesson1_output_mathematica"}];
If[!DirectoryQ[outputDirectory], CreateDirectory[outputDirectory]];
Do[
Export[
FileNameJoin[{outputDirectory, result["Case"]["Name"] <> ".csv"}],
Prepend[result["Data"], {"time", "x", "k"}]
],
{result, results}
];
Export[FileNameJoin[{outputDirectory, "state_response.png"}], statePlot, ImageResolution -> 180];
Export[FileNameJoin[{outputDirectory, "gain_response.png"}], gainPlot, ImageResolution -> 180];
{Dataset[summaries], statePlot, gainPlot}
]]], "Input"]
}, WindowTitle -> "Chapter16_Lesson1", Saveable -> True]
15. Interpretation and Verification Checks
15.1 Expected numerical behavior
- Correct-sign cases complete the full eight-second interval.
- The state approaches zero while the adaptive gain approaches a finite constant.
- Wrong-sign cases reach the safety threshold in less than one second.
- Changing both \(b\) and \(\hat{\sigma}\) leaves \(q=b\hat{\sigma}\) unchanged, so the corresponding trajectories coincide.
15.2 Analytical consistency check
In the correct-sign cases, \(q=1.5\). A sufficient stabilizing gain is any
\[ k^{*}>\frac{a}{q}=\frac{0.8}{1.5}\approx0.5333. \]
The computed gain rises from \(0.1\) and settles above this threshold. In the wrong-sign cases, \(c=1.5\), and the exact trajectory relation is
\[ x^2=1+1.6(k-0.1)+1.5(k^2-0.01). \]
Substituting a recorded gain into this equation provides a direct check on numerical integration accuracy before the safety event.
15.3 Engineering lesson
Larger adaptation gain is not automatically safer. When the feedback orientation is correct, increasing adaptation can establish damping more quickly. When the orientation is wrong, the same mechanism accelerates the destabilizing loop. Unknown-direction designs must therefore alter the gain structure itself rather than merely tuning \(\gamma\).
16. Problems and Solutions
Problem 1 (High-Frequency Gain): Determine the relative degree and high-frequency gain sign of
\[ G(s)=\frac{4-2s}{s^3+3s^2+5s+7}. \]
Solution:
The numerator degree is one and the denominator degree is three, so the relative degree is \(\rho=2\). Then
\[ k_{\mathrm{hf} }=\lim_{s\to\infty}s^2G(s) =\lim_{s\to\infty}\frac{-2s^3+4s^2} {s^3+3s^2+5s+7}=-2. \]
The high-frequency gain is negative. A high-bandwidth controller derived under a positive-sign assumption would reverse the intended action.
Problem 2 (No Common Constant Gain): For \(a=2\) and \(b\in\{+1,-1\}\), prove directly that no feedback \(u=-kx\) stabilizes both plants.
Solution:
For \(b=+1\), stability requires \(2-k<0\), hence \(k>2\). For \(b=-1\), stability requires \(2+k<0\), hence \(k<-2\). These inequalities cannot hold simultaneously.
Problem 3 (MRAC Sign Dependence): Starting from \(\dot e=a_m e+b\tilde{\boldsymbol{\theta} }^T \boldsymbol{\phi}\), derive an adaptive law that makes \(\dot V=a_m e^2\) for
\[ V=\frac12 e^2+\frac{|b|}{2\gamma} \tilde{\boldsymbol{\theta} }^T\tilde{\boldsymbol{\theta} }. \]
Solution:
Differentiation gives
\[ \dot V=a_m e^2+b e\tilde{\boldsymbol{\theta} }^T\boldsymbol{\phi} +\frac{|b|}{\gamma}\tilde{\boldsymbol{\theta} }^T \dot{\hat{\boldsymbol{\theta} } }. \]
Choose
\[ \dot{\hat{\boldsymbol{\theta} } } =-\gamma\operatorname{sgn}(b)\boldsymbol{\phi}e. \]
Then
\[ \frac{|b|}{\gamma}\tilde{\boldsymbol{\theta} }^T \dot{\hat{\boldsymbol{\theta} } } =-|b|\operatorname{sgn}(b) \tilde{\boldsymbol{\theta} }^T\boldsymbol{\phi}e =-b\tilde{\boldsymbol{\theta} }^T\boldsymbol{\phi}e, \]
which cancels the cross term. Thus \(\dot V=a_m e^2\le0\). The cancellation explicitly uses the sign of \(b\).
Problem 4 (Wrong-Sign Trajectory Invariant): For
\[ \dot x=(a+ck)x, \qquad \dot k=\gamma x^2, \]
derive the relationship between \(x^2\) and \(k\), and explain why the gain has finite escape.
Solution:
Using the chain rule,
\[ \frac{d(x^2)}{dk} =\frac{2x\dot x}{\dot k} =\frac{2(a+ck)x^2}{\gamma x^2} =\frac{2(a+ck)}{\gamma}. \]
Integration gives
\[ x^2=x_0^2+\frac{2a}{\gamma}(k-k_0) +\frac{c}{\gamma}(k^2-k_0^2). \]
Therefore,
\[ \dot k=\gamma x_0^2+2a(k-k_0)+c(k^2-k_0^2). \]
The right-hand side grows quadratically. Beyond a sufficiently large \(K\), it is at least \((c/2)k^2\). The comparison solution of \(\dot z=(c/2)z^2\) becomes unbounded after a finite interval, so \(k\) and \(x\) also have finite escape.
Problem 5 (Sign Estimation from Regression): Suppose \(a\) is unknown and measurements satisfy
\[ \dot x(t)=a x(t)+b u(t). \]
Write a batch least-squares estimate for \(\boldsymbol\theta=[a\;b]^T\) and state the condition required to identify the sign of \(b\) uniquely.
Solution:
Define the regressor and information matrix
\[ \boldsymbol\phi(t)=\begin{bmatrix}x(t)\\u(t)\end{bmatrix}, \qquad \mathbf R_T=\int_0^T\boldsymbol\phi(t) \boldsymbol\phi^T(t)\,dt. \]
If \(\mathbf R_T\) is nonsingular, then
\[ \hat{\boldsymbol\theta}= \mathbf R_T^{-1}\int_0^T\boldsymbol\phi(t)\dot x(t)\,dt. \]
Unique identification requires linearly independent excitation of the state and input regressors, equivalently positive definiteness of \(\mathbf R_T\). If \(u\) is zero or proportional to \(x\) over the entire interval, the matrix can be singular or ill-conditioned, and the sign of \(b\) cannot be reliably separated from \(a\).
17. Summary
The sign of the input or high-frequency gain determines the orientation of feedback. A single fixed gain cannot stabilize an unstable scalar plant for both possible directions. Standard Lyapunov MRAC exposes the same issue through a cross-term cancellation that requires \(\operatorname{sgn}(b)\). In the adaptive high-gain counterexample, a correct sign assumption gives bounded signals and asymptotic regulation, whereas a wrong sign assumption creates a self-reinforcing positive-feedback mechanism with finite escape. These results motivate the specially structured dynamic gains introduced in Lesson 2.
18. References
- Morse, A.S. (1980). Global stability of parameter-adaptive control systems. IEEE Transactions on Automatic Control, 25(3), 433-439.
- Morse, A.S. (1983). Recent problems in parameter adaptive control. In I.D. Landau (Ed.), Outils et Modèles Mathématiques pour l'Automatique, l'Analyse de Systèmes et le Traitement du Signal, Vol. 3, 733-740.
- Nussbaum, R.D. (1983). Some remarks on a conjecture in parameter adaptive control. Systems & Control Letters, 3(5), 243-246.
- Willems, J.C., & Byrnes, C.I. (1984). Global adaptive stabilization in the absence of information on the sign of the high frequency gain. In Analysis and Optimization of Systems, Lecture Notes in Control and Information Sciences, Vol. 62, 49-57.
- Mudgett, D.R., & Morse, A.S. (1985). Adaptive stabilization of linear systems with unknown high-frequency gain. IEEE Transactions on Automatic Control, 30(6), 549-554.
- Heymann, M., Lewis, J.H., & Meyer, G. (1985). Remarks on the adaptive control of linear plants with unknown high-frequency gain. Systems & Control Letters, 5(6), 357-362.
- Mårtensson, B. (1990). Remarks on adaptive stabilization of first-order nonlinear systems. Systems & Control Letters, 14(1), 1-7.
- Ye, X., & Jiang, J. (1998). Adaptive nonlinear design without a priori knowledge of control directions. IEEE Transactions on Automatic Control, 43(11), 1617-1621.
- Ye, X. (1999). Asymptotic regulation of time-varying uncertain nonlinear systems with unknown control directions. Automatica, 35(5), 929-935.
- Ye, X. (2001). Adaptive nonlinear output-feedback control with unknown high-frequency gain sign. IEEE Transactions on Automatic Control, 46(1), 112-115.
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.