Chapter 16: Adaptive Control of Systems with Unknown High-Frequency Gain Sign
Lesson 4: Practical Limitations and Implementation Challenges
This lesson examines the gap between the ideal continuous-time analysis of Nussbaum-gain adaptive controllers and their implementation with sampled measurements, finite-precision arithmetic, actuator constraints, delays, disturbances, and supervisory safety logic. The objective is not to replace the stability theory developed in Lessons 1–3, but to identify exactly which assumptions are altered by a practical realization and what must be monitored, tested, or re-proved before deployment.
1. Learning Objectives and Scope
After completing this lesson, students should be able to:
- reconstruct the ideal scalar Nussbaum-gain stability argument;
- identify where actuator saturation, rate limits, and dead zones enter the Lyapunov derivative;
- derive noise-induced drift in the Nussbaum search variable;
- state a sample-time restriction for a locally frozen discrete implementation;
- explain why time-varying or sign-changing control effectiveness is not covered by the standard constant-sign theorem;
- design numerical guards, monitors, and staged validation tests without claiming that these additions preserve the original proof automatically.
The lesson uses the scalar plant as the main analytical vehicle because it exposes the implementation mechanisms without introducing new nonlinear control structures beyond those already developed in the preceding lessons.
2. Ideal Continuous-Time Result Revisited
Consider the regulation model \( \dot{x}=b u \), where the constant control coefficient \( b\in\mathbb{R}\setminus\{0\} \) is unknown and its sign is not available to the controller. Select
\[ u=N(\kappa)x, \qquad \dot{\kappa}=x^2, \qquad \kappa(0)=\kappa_0, \]
with a Nussbaum-type function \(N\). A standard defining property is
\[ \limsup_{q\rightarrow\infty}\frac{1}{q}\int_0^q N(s)\,ds=+\infty, \qquad \liminf_{q\rightarrow\infty}\frac{1}{q}\int_0^q N(s)\,ds=-\infty. \]
The moderate-growth choice used in the implementations is \(N(\kappa)=\kappa^2\cos\kappa\). Its primitive is
\[ \int_0^q s^2\cos s\,ds=q^2\sin q+2q\cos q-2\sin q, \]
and division by \(q\) produces oscillations whose positive and negative envelopes grow without bound.
2.1 Proof Sketch
With \(V=\tfrac12 x^2\),
\[ \dot V=x\dot x=bN(\kappa)x^2=bN(\kappa)\dot\kappa. \]
Integration gives
\[ V(t)=V(0)+b\int_{\kappa_0}^{\kappa(t)}N(s)\,ds. \]
Suppose \(\kappa(t)\) were unbounded. Because \(\dot\kappa=x^2\ge 0\), it would pass through arbitrarily large values. The Nussbaum property guarantees a subsequence at which the integral term has the sign opposite to \(b\) and magnitude exceeding \(V(0)\). The right-hand side would then be negative, contradicting \(V(t)\ge 0\). Hence \(\kappa\) is bounded. Since \(\int_0^\infty x^2(t)dt=\kappa(\infty)-\kappa_0<\infty\), and bounded closed-loop signals imply bounded \(\dot x\), Barbalat's lemma yields \(x(t)\rightarrow 0\).
Every practical limitation discussed below can be interpreted as a change to one of the equalities or boundedness implications used in this proof.
3. The Implemented Loop Is a Different Dynamical System
A sampled implementation typically has the form
\[ \begin{aligned} y_k &= x_k+n_k,\\ u_k^{\mathrm{cmd}} &= N(\kappa_k)y_k,\\ u_k &= \operatorname{sat}_{u_{\max}}\!\left(u_k^{\mathrm{cmd}}\right),\\ \kappa_{k+1} &= \kappa_k+h\gamma\psi(y_k),\\ x_{k+1} &= x_k+h\left(a x_k+b u_k+d_k\right), \end{aligned} \]
where the practical dead-zone adaptation map may be
\[ \psi(y)=\left(\max\{|y|-\varepsilon,0\}\right)^2. \]
This realization contains noise \(n_k\), a finite sample period \(h\), actuator saturation, disturbance \(d_k\), and a modified update law. It is therefore not a numerical transcription of the ideal proof; it is a hybrid nonlinear system that requires its own analysis or, at minimum, conservative validation.
flowchart TD
R["Reference or zero command"] --> E["Tracking error"]
Y["Sensor, noise, quantization"] --> E
E --> DZ["Dead-zone and normalization"]
DZ --> K["Kappa update"]
K --> N["Nussbaum map N(kappa)"]
E --> M["Control multiplication"]
N --> M
M --> A["Saturation and rate limit"]
A --> P["Plant with unknown input sign"]
P --> Y
K --> S["Supervisory monitors"]
A --> S
Y --> S
S --> F["Safe fallback or shutdown"]
4. Actuator Saturation, Rate Limits, and Lost Lyapunov Cancellation
Let the applied input be decomposed as
\[ u_{\mathrm{act}}=u_{\mathrm{cmd}}+\Delta u, \qquad \Delta u=\operatorname{sat}(u_{\mathrm{cmd}})-u_{\mathrm{cmd}}. \]
For the ideal scalar plant and update law, the Lyapunov derivative becomes
\[ \dot V=bN(\kappa)\dot\kappa+b x\Delta u. \]
The first term is the Nussbaum term used in the contradiction argument. The second term has no guaranteed sign. Applying Young's inequality for any \(\rho>0\),
\[ |b x\Delta u|\le \frac{\rho}{2}x^2+ \frac{b^2}{2\rho}(\Delta u)^2. \]
If saturation persists, \((\Delta u)^2\) need not be integrable, so asymptotic convergence cannot be recovered from the ideal proof. At best, one may derive ultimate boundedness under additional bounds and sufficient control authority. A rate limiter produces a dynamic mismatch rather than a static one, but it enters the Lyapunov analysis through the same applied-versus-commanded input defect.
4.1 Why Nussbaum Search Can Produce Severe Transients
The search variable must move through intervals in which the effective gain \(bN(\kappa)\) is destabilizing before it reaches a stabilizing interval. For \(N(\kappa)=\kappa^2\cos\kappa\), the command envelope scales approximately as
\[ |u_{\mathrm{cmd}}|\le \kappa^2|x|. \]
Larger \(\kappa\) therefore increases both the chance of finding the correct direction and the risk of saturation, excitation of neglected dynamics, and numerical overflow. This is a fundamental transient trade-off, not merely a software tuning issue.
5. Measurement Noise and Monotone Drift of the Search Variable
With measured error \(y=e+n\) and the unmodified law \(\dot\kappa=\gamma y^2\), assume zero-mean noise independent of the current tracking error, with variance \(\mathbb{E}[n^2]=\sigma_n^2\). Then
\[ \begin{aligned} \mathbb{E}[\dot\kappa\mid e] &=\gamma\mathbb{E}[(e+n)^2\mid e]\\ &=\gamma(e^2+2e\mathbb{E}[n]+\mathbb{E}[n^2])\\ &=\gamma(e^2+\sigma_n^2). \end{aligned} \]
Even at perfect regulation, \(e=0\), the expected drift is \(\gamma\sigma_n^2>0\). Consequently, \(\kappa\) continues to move, the Nussbaum gain continues to change sign and amplitude, and the controller may leave a previously benign operating interval.
5.1 Dead Zone and Leakage Are Modifications, Not Free Fixes
A dead-zone law such as
\[ \dot\kappa=\gamma\left(\max\{|y|-\varepsilon,0\}\right)^2 \]
suppresses small-noise updates, but it creates a non-adapting region and generally changes the conclusion from exact convergence to convergence into a neighborhood. A leakage term \(\dot\kappa=\gamma y^2-\sigma\kappa\) prevents monotone growth but destroys the monotonicity used in the standard Nussbaum lemma. Either modification requires a revised boundedness proof.
Filtering can reduce high-frequency noise, but it introduces phase lag. The filter state must be included in the closed-loop model; otherwise the controller is analyzed using a signal that is not actually applied.
6. Sampling, Zero-Order Hold, and Numerical Integration
Freeze \(N(\kappa_k)\) over one sampling interval and ignore noise, saturation, and disturbances. Explicit Euler gives
\[ x_{k+1}=\left[1+h\lambda_k\right]x_k, \qquad \lambda_k=a+bN(\kappa_k). \]
For real \(\lambda_k\), local Euler stability requires
\[ |1+h\lambda_k|<1 \quad\Longleftrightarrow\quad -2<h\lambda_k<0. \]
A continuous-time interval with \(\lambda_k<0\) can therefore become numerically unstable when the gain magnitude grows while the sample period remains fixed. Since a Nussbaum function intentionally generates large gain magnitudes, the sample time must be selected from the largest credible closed-loop bandwidth, not merely from the nominal plant dynamics.
6.1 Discrete Update Effects
The implemented adaptation step
\[ \kappa_{k+1}=\kappa_k+h\gamma\psi(y_k) \]
can jump across a favorable interval of \(N(\kappa)\) when \(h\gamma\psi(y_k)\) is too large. A useful engineering constraint is
\[ h\gamma\psi(y_k)\le \Delta\kappa_{\max}, \]
where \(\Delta\kappa_{\max}\) is selected to resolve the oscillation scale of the chosen Nussbaum function. Limiting this increment is another modification of the ideal law and must be documented explicitly.
7. Delay, Unmodeled Dynamics, and Relative-Degree Errors
Suppose the actuator or computation introduces delay \(\tau\), so that the plant receives \(u(t-\tau)\). For a smooth error signal,
\[ e(t-\tau)=e(t)-\tau\dot e(t)+\mathcal{O}(\tau^2). \]
The nominal product \(bN(\kappa)e^2\) is then accompanied by terms involving \(-\tau bN(\kappa)e\dot e\). Because the Nussbaum amplitude can grow, even a small delay may generate a large phase error. The same concern arises from neglected actuator poles, flexible modes, sensor filters, and transport lags.
Unknown high-frequency gain sign theory also assumes a known relative degree and a valid input-output model structure. If an unmodeled pole changes the effective phase near the adaptive-loop bandwidth, the controller may interpret the phase error as an incorrect control direction and increase the search gain, thereby exciting the neglected mode further.
Practical validation must therefore include frequency-domain uncertainty, not only parameter variation in the nominal low-order model.
8. Vanishing, Time-Varying, or Sign-Changing Control Effectiveness
The standard scalar proof assumes a constant nonzero coefficient \(b\). If the actual plant has \(b=b(t)\), then
\[ \dot V=b(t)N(\kappa)\dot\kappa. \]
This expression cannot generally be integrated into a fixed constant times the primitive of \(N\). If \(b(t)\) crosses zero, control authority disappears at the crossing and the control direction reverses afterward. A controller proven for an unknown but constant sign must not be advertised as covering arbitrary sign changes.
Stronger Nussbaum-function definitions and specialized integral lemmas exist for restricted classes of time-varying coefficients, but they impose explicit conditions on the coefficient and on the chosen function. Those results are separate designs, not automatic extensions of the constant-sign theorem.
9. Scaling, Initialization, and Finite-Precision Arithmetic
9.1 Signal Scaling
If the physical error is measured in large units, the update \(\dot\kappa=\gamma e^2\) can be excessively fast. Introduce a dimensionless normalized error
\[ e_s=\frac{e}{s_e}, \qquad \dot\kappa=\gamma_s e_s^2, \qquad u=s_u N(\kappa)e_s, \]
where \(s_e\) and \(s_u\) reflect credible error and actuator scales. This avoids embedding unit choices inside the adaptation gain.
9.2 Initial Search Value
For \(N(0)=0\), choosing \(\kappa_0=0\) initially applies no control. An unstable plant may grow before adaptation generates useful authority. Conversely, a large \(|\kappa_0|\) can create an immediate command spike. A small nonzero initial value should be selected together with an initial-state envelope and actuator limit.
9.3 Overflow and Loss of Significance
Polynomial Nussbaum functions grow more slowly than exponential choices, but even \(\kappa^2\cos\kappa\) can overflow after prolonged drift. Software should test \(\operatorname{isfinite}(\kappa)\), \(\operatorname{isfinite}(N(\kappa))\), and the commanded input before multiplication reaches the actuator interface. Silent numeric wraparound can invert the effective control direction without any physical change in the plant.
10. Supervisory Logic and What It Can Legitimately Claim
A practical controller should expose the following variables to a supervisory layer: tracking error, \(\kappa\), \(N(\kappa)\), commanded and applied input, saturation duty cycle, adaptation increment, sample overruns, and finite-value flags.
A trip condition may freeze adaptation and command a safe fallback when \(|\kappa|\), control effort, state magnitude, or saturation duration exceeds validated limits. However, clipping \(\kappa\) or resetting it periodically destroys the exact integral identity used by the Nussbaum proof. Such logic is acceptable as a safety mechanism only when the fallback behavior is independently safe and the controller's formal claim is reduced accordingly.
For an open-loop unstable plant, setting \(u=0\) may not be a safe fallback. The fallback may instead be a mechanical shutdown, a known low-authority mode, a redundant sign-verified controller, or a supervisory operating-point transition. The safe action is plant-specific.
11. Staged Verification Workflow
flowchart TD
A["Prove ideal continuous-time model"] --> B["Bound initial state and plant parameters"]
B --> C["Add actuator amplitude and rate models"]
C --> D["Add sensor noise, quantization, and filters"]
D --> E["Select sample time and numeric precision"]
E --> F["Monte Carlo simulation for both input signs"]
F --> G["Worst-case delay and unmodeled-mode tests"]
G --> H["Processor-in-the-loop timing tests"]
H --> I["Hardware-in-the-loop with physical limits"]
I --> J["Low-energy experiment with independent shutdown"]
J --> K["Document validated operating envelope"]
The test matrix must include both signs of the control coefficient, extreme credible magnitudes of that coefficient, initial-condition corners, sensor bias and noise, saturation persistence, delay variation, missed samples, and parameter combinations that excite neglected dynamics. A successful nominal simulation is not evidence of sign-independent robustness.
12. Implementation Design Rules
- Use a moderate-growth Nussbaum function first. Exponential functions may simplify some proofs but can be numerically hazardous.
- Normalize measured errors. Adaptation gains should not depend implicitly on engineering units.
- Log commanded and applied input separately. Their difference is the actuator defect that invalidates nominal cancellation.
- Choose the sample time from the maximum adaptive bandwidth. The nominal plant bandwidth alone is insufficient.
- Do not silently clamp or reset the search variable. Treat every bound, freeze, or reset as a hybrid-mode transition requiring a revised claim.
- Provide an independent safety path. The adaptive controller must not be the only mechanism capable of removing energy or stopping the experiment.
- Validate both signs. Code that works only for the sign used during development does not solve the unknown-direction problem.
- Report the operating envelope. State bounds, noise bounds, delay bounds, actuator limits, and sample-time limits belong in the controller specification.
13. Python Implementation
The Python implementation uses NumPy for numerical arrays
and random noise, and Matplotlib for separate state,
search-variable, and actuator plots. It simulates both positive and
negative input gains and exports CSV logs.
Chapter16_Lesson4.py
"""Chapter16_Lesson4.py
Practical simulation of a scalar Nussbaum-gain controller for an unknown
high-frequency gain sign. The script compares both signs of b and records
measurement noise, actuator saturation, an adaptation dead zone, and a
supervisory trip. The safeguards are engineering devices; they do not preserve
the ideal continuous-time proof automatically.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Dict
import matplotlib.pyplot as plt
import numpy as np
@dataclass(frozen=True)
class Config:
dt: float = 1.0e-3
duration: float = 20.0
a: float = 0.25
gamma: float = 0.20
kappa0: float = 0.50
x0: float = 1.50
noise_std: float = 0.01
dead_zone: float = 0.03
u_max: float = 8.0
kappa_trip: float = 40.0
seed: int = 7
def nussbaum(kappa: float) -> float:
"""Moderate-growth Nussbaum function N(kappa) = kappa^2 cos(kappa)."""
return kappa * kappa * np.cos(kappa)
def saturate(value: float, limit: float) -> float:
return float(np.clip(value, -limit, limit))
def simulate(b: float, cfg: Config) -> Dict[str, np.ndarray]:
if abs(b) < 1.0e-12:
raise ValueError("The control coefficient b must be nonzero.")
if cfg.dt <= 0.0 or cfg.duration <= 0.0:
raise ValueError("dt and duration must be positive.")
steps = int(round(cfg.duration / cfg.dt)) + 1
t = np.linspace(0.0, cfg.duration, steps)
x = np.zeros(steps)
y = np.zeros(steps)
kappa = np.zeros(steps)
gain = np.zeros(steps)
u_cmd = np.zeros(steps)
u = np.zeros(steps)
saturation_active = np.zeros(steps, dtype=bool)
safety_trip = np.zeros(steps, dtype=bool)
x[0] = cfg.x0
kappa[0] = cfg.kappa0
rng = np.random.default_rng(cfg.seed)
tripped = False
for i in range(steps - 1):
y[i] = x[i] + cfg.noise_std * rng.standard_normal()
error = y[i]
gain[i] = nussbaum(kappa[i])
u_cmd[i] = gain[i] * error
u[i] = saturate(u_cmd[i], cfg.u_max)
saturation_active[i] = not np.isclose(u[i], u_cmd[i])
excess_error = max(abs(error) - cfg.dead_zone, 0.0)
kappa_dot = cfg.gamma * excess_error * excess_error
if kappa[i] >= cfg.kappa_trip or not np.isfinite(gain[i]):
tripped = True
if tripped:
u[i] = 0.0
kappa_dot = 0.0
safety_trip[i] = True
disturbance = 0.02 * np.sin(1.4 * t[i])
x_dot = cfg.a * x[i] + b * u[i] + disturbance
# Explicit Euler is intentionally visible here because its sample-time
# sensitivity is one of the lesson's implementation topics.
x[i + 1] = x[i] + cfg.dt * x_dot
kappa[i + 1] = kappa[i] + cfg.dt * kappa_dot
if not np.isfinite(x[i + 1]) or abs(x[i + 1]) > 1.0e6:
raise FloatingPointError("Simulation diverged or overflowed.")
y[-1] = x[-1] + cfg.noise_std * rng.standard_normal()
gain[-1] = nussbaum(kappa[-1])
u_cmd[-1] = gain[-1] * y[-1]
u[-1] = saturate(u_cmd[-1], cfg.u_max)
saturation_active[-1] = not np.isclose(u[-1], u_cmd[-1])
safety_trip[-1] = tripped
return {
"t": t,
"x": x,
"y": y,
"kappa": kappa,
"gain": gain,
"u_cmd": u_cmd,
"u": u,
"saturation": saturation_active.astype(int),
"trip": safety_trip.astype(int),
}
def save_csv(result: Dict[str, np.ndarray], path: Path) -> None:
columns = ["t", "x", "y", "kappa", "gain", "u_cmd", "u", "saturation", "trip"]
matrix = np.column_stack([result[name] for name in columns])
np.savetxt(path, matrix, delimiter=",", header=",".join(columns), comments="")
def print_metrics(label: str, result: Dict[str, np.ndarray]) -> None:
x = result["x"]
u = result["u"]
sat_fraction = float(np.mean(result["saturation"]))
print(
f"{label}: final_x={x[-1]: .6f}, max_abs_x={np.max(np.abs(x)): .6f}, "
f"final_kappa={result['kappa'][-1]: .6f}, max_abs_u={np.max(np.abs(u)): .6f}, "
f"sat_fraction={sat_fraction: .4f}, tripped={bool(result['trip'][-1])}"
)
def plot_result(label: str, result: Dict[str, np.ndarray]) -> None:
t = result["t"]
plt.figure()
plt.plot(t, result["x"], label="state x")
plt.plot(t, result["y"], alpha=0.35, label="measured y")
plt.xlabel("time [s]")
plt.ylabel("state")
plt.title(f"State response: {label}")
plt.grid(True)
plt.legend()
plt.figure()
plt.plot(t, result["kappa"], label="kappa")
plt.plot(t, result["gain"], label="N(kappa)")
plt.xlabel("time [s]")
plt.ylabel("adaptive variables")
plt.title(f"Nussbaum search: {label}")
plt.grid(True)
plt.legend()
plt.figure()
plt.plot(t, result["u_cmd"], label="commanded input")
plt.plot(t, result["u"], label="applied input")
plt.xlabel("time [s]")
plt.ylabel("control input")
plt.title(f"Actuator effect: {label}")
plt.grid(True)
plt.legend()
def main() -> None:
cfg = Config()
output_dir = Path("Chapter16_Lesson4_output")
output_dir.mkdir(exist_ok=True)
cases = {"b_positive": 1.5, "b_negative": -1.5}
for label, b in cases.items():
result = simulate(b, cfg)
print_metrics(label, result)
save_csv(result, output_dir / f"{label}.csv")
plot_result(label, result)
plt.show()
if __name__ == "__main__":
main()
14. C++17 Implementation
The C++ implementation uses only the standard library. It performs explicit bounds and finite-value checks and writes one CSV file for each possible sign.
Chapter16_Lesson4.cpp
// Chapter16_Lesson4.cpp
// C++17 implementation of a sampled Nussbaum-gain controller with practical
// safeguards. The safety devices modify the ideal theorem and therefore need
// independent analysis before use on hardware.
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <random>
#include <stdexcept>
#include <string>
#include <vector>
struct Config {
double dt = 1.0e-3;
double duration = 20.0;
double a = 0.25;
double gamma = 0.20;
double kappa0 = 0.50;
double x0 = 1.50;
double noiseStd = 0.01;
double deadZone = 0.03;
double uMax = 8.0;
double kappaTrip = 40.0;
unsigned int seed = 7U;
};
struct Sample {
double t{};
double x{};
double y{};
double kappa{};
double gain{};
double uCommand{};
double uApplied{};
int saturation{};
int trip{};
};
double nussbaum(const double kappa) {
return kappa * kappa * std::cos(kappa);
}
double saturate(const double value, const double limit) {
return std::clamp(value, -limit, limit);
}
std::vector<Sample> simulate(const double b, const Config& cfg) {
if (std::abs(b) < 1.0e-12) {
throw std::invalid_argument("The control coefficient b must be nonzero.");
}
if (cfg.dt <= 0.0 || cfg.duration <= 0.0) {
throw std::invalid_argument("dt and duration must be positive.");
}
const std::size_t steps =
static_cast<std::size_t>(std::llround(cfg.duration / cfg.dt)) + 1U;
std::vector<Sample> data(steps);
std::mt19937 generator(cfg.seed);
std::normal_distribution<double> normal(0.0, cfg.noiseStd);
double x = cfg.x0;
double kappa = cfg.kappa0;
bool tripped = false;
for (std::size_t i = 0; i < steps; ++i) {
const double t = static_cast<double>(i) * cfg.dt;
const double y = x + normal(generator);
const double error = y;
const double gain = nussbaum(kappa);
const double uCommand = gain * error;
double uApplied = saturate(uCommand, cfg.uMax);
const bool saturated = std::abs(uApplied - uCommand) > 1.0e-12;
const double excessError = std::max(std::abs(error) - cfg.deadZone, 0.0);
double kappaDot = cfg.gamma * excessError * excessError;
if (kappa >= cfg.kappaTrip || !std::isfinite(gain)) {
tripped = true;
}
if (tripped) {
uApplied = 0.0;
kappaDot = 0.0;
}
data[i] = Sample{t,
x,
y,
kappa,
gain,
uCommand,
uApplied,
saturated ? 1 : 0,
tripped ? 1 : 0};
if (i + 1U < steps) {
const double disturbance = 0.02 * std::sin(1.4 * t);
const double xDot = cfg.a * x + b * uApplied + disturbance;
// Explicit Euler is intentionally retained to expose sample-time
// sensitivity in the lesson.
x += cfg.dt * xDot;
kappa += cfg.dt * kappaDot;
if (!std::isfinite(x) || std::abs(x) > 1.0e6) {
throw std::runtime_error("Simulation diverged or overflowed.");
}
}
}
return data;
}
void saveCsv(const std::vector<Sample>& data, const std::string& filename) {
std::ofstream file(filename);
if (!file) {
throw std::runtime_error("Could not open output file: " + filename);
}
file << "t,x,y,kappa,gain,u_cmd,u,saturation,trip\n";
file << std::setprecision(12);
for (const Sample& s : data) {
file << s.t << ',' << s.x << ',' << s.y << ',' << s.kappa << ','
<< s.gain << ',' << s.uCommand << ',' << s.uApplied << ','
<< s.saturation << ',' << s.trip << '\n';
}
}
void printMetrics(const std::string& label, const std::vector<Sample>& data) {
double maxAbsX = 0.0;
double maxAbsU = 0.0;
std::size_t saturationCount = 0U;
for (const Sample& s : data) {
maxAbsX = std::max(maxAbsX, std::abs(s.x));
maxAbsU = std::max(maxAbsU, std::abs(s.uApplied));
saturationCount += static_cast<std::size_t>(s.saturation);
}
const Sample& last = data.back();
const double saturationFraction =
static_cast<double>(saturationCount) / static_cast<double>(data.size());
std::cout << std::fixed << std::setprecision(6)
<< label << ": final_x=" << last.x
<< ", max_abs_x=" << maxAbsX
<< ", final_kappa=" << last.kappa
<< ", max_abs_u=" << maxAbsU
<< ", sat_fraction=" << saturationFraction
<< ", tripped=" << (last.trip != 0 ? "true" : "false") << '\n';
}
int main() {
try {
const Config cfg{};
const std::vector<std::pair<std::string, double>> cases{
{"b_positive", 1.5},
{"b_negative", -1.5},
};
for (const auto& item : cases) {
const auto result = simulate(item.second, cfg);
printMetrics(item.first, result);
saveCsv(result, "Chapter16_Lesson4_" + item.first + ".csv");
}
} catch (const std::exception& error) {
std::cerr << "Error: " << error.what() << '\n';
return 1;
}
return 0;
}
15. Java Implementation
The Java implementation targets Java 17 and uses standard classes for random noise, CSV output, validation, and exception handling.
Chapter16_Lesson4.java
// Chapter16_Lesson4.java
// Java 17 implementation of a sampled Nussbaum-gain controller with practical
// safeguards. The safeguards modify the ideal continuous-time theorem and must
// be analyzed independently before hardware deployment.
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
public final class Chapter16_Lesson4 {
private Chapter16_Lesson4() {
}
private static final class Config {
final double dt = 1.0e-3;
final double duration = 20.0;
final double a = 0.25;
final double gamma = 0.20;
final double kappa0 = 0.50;
final double x0 = 1.50;
final double noiseStd = 0.01;
final double deadZone = 0.03;
final double uMax = 8.0;
final double kappaTrip = 40.0;
final long seed = 7L;
}
private static final class Sample {
final double t;
final double x;
final double y;
final double kappa;
final double gain;
final double uCommand;
final double uApplied;
final int saturation;
final int trip;
Sample(
double t,
double x,
double y,
double kappa,
double gain,
double uCommand,
double uApplied,
int saturation,
int trip) {
this.t = t;
this.x = x;
this.y = y;
this.kappa = kappa;
this.gain = gain;
this.uCommand = uCommand;
this.uApplied = uApplied;
this.saturation = saturation;
this.trip = trip;
}
}
private static double nussbaum(double kappa) {
return kappa * kappa * Math.cos(kappa);
}
private static double saturate(double value, double limit) {
return Math.max(-limit, Math.min(limit, value));
}
private static List<Sample> simulate(double b, Config cfg) {
if (Math.abs(b) < 1.0e-12) {
throw new IllegalArgumentException("The control coefficient b must be nonzero.");
}
if (cfg.dt <= 0.0 || cfg.duration <= 0.0) {
throw new IllegalArgumentException("dt and duration must be positive.");
}
int steps = (int) Math.round(cfg.duration / cfg.dt) + 1;
List<Sample> data = new ArrayList<>(steps);
Random random = new Random(cfg.seed);
double x = cfg.x0;
double kappa = cfg.kappa0;
boolean tripped = false;
for (int i = 0; i < steps; i++) {
double t = i * cfg.dt;
double y = x + cfg.noiseStd * random.nextGaussian();
double error = y;
double gain = nussbaum(kappa);
double uCommand = gain * error;
double uApplied = saturate(uCommand, cfg.uMax);
boolean saturated = Math.abs(uApplied - uCommand) > 1.0e-12;
double excessError = Math.max(Math.abs(error) - cfg.deadZone, 0.0);
double kappaDot = cfg.gamma * excessError * excessError;
if (kappa >= cfg.kappaTrip || !Double.isFinite(gain)) {
tripped = true;
}
if (tripped) {
uApplied = 0.0;
kappaDot = 0.0;
}
data.add(new Sample(
t,
x,
y,
kappa,
gain,
uCommand,
uApplied,
saturated ? 1 : 0,
tripped ? 1 : 0));
if (i + 1 < steps) {
double disturbance = 0.02 * Math.sin(1.4 * t);
double xDot = cfg.a * x + b * uApplied + disturbance;
// Explicit Euler is intentionally retained to expose sample-time
// sensitivity in the lesson.
x += cfg.dt * xDot;
kappa += cfg.dt * kappaDot;
if (!Double.isFinite(x) || Math.abs(x) > 1.0e6) {
throw new ArithmeticException("Simulation diverged or overflowed.");
}
}
}
return data;
}
private static void saveCsv(List<Sample> data, Path path) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(
path, StandardCharsets.UTF_8)) {
writer.write("t,x,y,kappa,gain,u_cmd,u,saturation,trip");
writer.newLine();
for (Sample s : data) {
writer.write(String.format(
Locale.ROOT,
"%.12g,%.12g,%.12g,%.12g,%.12g,%.12g,%.12g,%d,%d",
s.t,
s.x,
s.y,
s.kappa,
s.gain,
s.uCommand,
s.uApplied,
s.saturation,
s.trip));
writer.newLine();
}
}
}
private static void printMetrics(String label, List<Sample> data) {
double maxAbsX = 0.0;
double maxAbsU = 0.0;
int saturationCount = 0;
for (Sample s : data) {
maxAbsX = Math.max(maxAbsX, Math.abs(s.x));
maxAbsU = Math.max(maxAbsU, Math.abs(s.uApplied));
saturationCount += s.saturation;
}
Sample last = data.get(data.size() - 1);
double saturationFraction = (double) saturationCount / data.size();
System.out.printf(
Locale.ROOT,
"%s: final_x=% .6f, max_abs_x=% .6f, final_kappa=% .6f, "
+ "max_abs_u=% .6f, sat_fraction=% .4f, tripped=%s%n",
label,
last.x,
maxAbsX,
last.kappa,
maxAbsU,
saturationFraction,
last.trip != 0);
}
public static void main(String[] args) {
Config cfg = new Config();
String[] labels = {"b_positive", "b_negative"};
double[] gains = {1.5, -1.5};
try {
for (int i = 0; i < labels.length; i++) {
List<Sample> result = simulate(gains[i], cfg);
printMetrics(labels[i], result);
saveCsv(result, Path.of("Chapter16_Lesson4_" + labels[i] + ".csv"));
}
} catch (IOException | RuntimeException error) {
System.err.println("Error: " + error.getMessage());
System.exit(1);
}
}
}
16. MATLAB and Simulink Implementation
The MATLAB script simulates the guarded sampled controller and can optionally construct a Simulink model containing the continuous plant, Nussbaum map, saturation, dead-zone adaptation, and a fixed-step solver. The generated core model is intended for extension with plant-specific noise, delay, rate-limit, and supervisory subsystems.
Chapter16_Lesson4.m
%% Chapter16_Lesson4.m
% Practical implementation study for a Nussbaum-gain controller with an
% unknown high-frequency gain sign. The numerical experiment includes
% measurement noise, actuator saturation, an adaptation dead zone, and a
% supervisory trip. These safeguards modify the ideal theorem and therefore
% require independent analysis before hardware deployment.
clear; clc; close all;
cfg.dt = 1.0e-3;
cfg.duration = 20.0;
cfg.a = 0.25;
cfg.gamma = 0.20;
cfg.kappa0 = 0.50;
cfg.x0 = 1.50;
cfg.noiseStd = 0.01;
cfg.deadZone = 0.03;
cfg.uMax = 8.0;
cfg.kappaTrip = 40.0;
cfg.seed = 7;
cases = struct('label', {'b_positive', 'b_negative'}, ...
'b', {1.5, -1.5});
for caseIndex = 1:numel(cases)
result = simulateNussbaum(cases(caseIndex).b, cfg);
printMetrics(cases(caseIndex).label, result);
outputTable = table(result.t, result.x, result.y, result.kappa, ...
result.gain, result.uCommand, result.uApplied, ...
result.saturation, result.trip, ...
'VariableNames', {'t', 'x', 'y', 'kappa', 'gain', 'u_cmd', ...
'u', 'saturation', 'trip'});
writetable(outputTable, ...
['Chapter16_Lesson4_' cases(caseIndex).label '.csv']);
figure('Name', ['State response: ' cases(caseIndex).label]);
plot(result.t, result.x, 'LineWidth', 1.2); hold on;
plot(result.t, result.y, 'LineWidth', 0.7);
xlabel('time [s]'); ylabel('state'); grid on;
legend('state x', 'measured y');
title(['State response: ' strrep(cases(caseIndex).label, '_', ' ')]);
figure('Name', ['Nussbaum search: ' cases(caseIndex).label]);
plot(result.t, result.kappa, 'LineWidth', 1.2); hold on;
plot(result.t, result.gain, 'LineWidth', 1.0);
xlabel('time [s]'); ylabel('adaptive variables'); grid on;
legend('\kappa', 'N(\kappa)');
title(['Nussbaum search: ' strrep(cases(caseIndex).label, '_', ' ')]);
figure('Name', ['Actuator effect: ' cases(caseIndex).label]);
plot(result.t, result.uCommand, 'LineWidth', 0.8); hold on;
plot(result.t, result.uApplied, 'LineWidth', 1.2);
xlabel('time [s]'); ylabel('control input'); grid on;
legend('commanded input', 'applied input');
title(['Actuator effect: ' strrep(cases(caseIndex).label, '_', ' ')]);
end
% Set this flag to true when Simulink is installed. The generated model
% contains the continuous plant, Nussbaum map, actuator saturation, dead-zone
% adaptation, and fixed-step solver configuration.
buildSimulinkModel = false;
if buildSimulinkModel
buildNussbaumSimulinkModel(cfg, -1.5);
end
function result = simulateNussbaum(b, cfg)
validateattributes(b, {'double'}, {'scalar', 'real', 'finite', 'nonzero'});
validateattributes(cfg.dt, {'double'}, {'scalar', 'positive', 'finite'});
validateattributes(cfg.duration, {'double'}, {'scalar', 'positive', 'finite'});
steps = round(cfg.duration / cfg.dt) + 1;
t = linspace(0.0, cfg.duration, steps).';
x = zeros(steps, 1);
y = zeros(steps, 1);
kappa = zeros(steps, 1);
gain = zeros(steps, 1);
uCommand = zeros(steps, 1);
uApplied = zeros(steps, 1);
saturation = false(steps, 1);
trip = false(steps, 1);
x(1) = cfg.x0;
kappa(1) = cfg.kappa0;
rng(cfg.seed, 'twister');
tripped = false;
for i = 1:(steps - 1)
y(i) = x(i) + cfg.noiseStd * randn();
error = y(i);
gain(i) = nussbaum(kappa(i));
uCommand(i) = gain(i) * error;
uApplied(i) = min(max(uCommand(i), -cfg.uMax), cfg.uMax);
saturation(i) = abs(uApplied(i) - uCommand(i)) > 1.0e-12;
excessError = max(abs(error) - cfg.deadZone, 0.0);
kappaDot = cfg.gamma * excessError^2;
if kappa(i) >= cfg.kappaTrip || ~isfinite(gain(i))
tripped = true;
end
if tripped
uApplied(i) = 0.0;
kappaDot = 0.0;
trip(i) = true;
end
disturbance = 0.02 * sin(1.4 * t(i));
xDot = cfg.a * x(i) + b * uApplied(i) + disturbance;
% Explicit Euler is intentionally retained so that sample-time
% sensitivity remains visible.
x(i + 1) = x(i) + cfg.dt * xDot;
kappa(i + 1) = kappa(i) + cfg.dt * kappaDot;
if ~isfinite(x(i + 1)) || abs(x(i + 1)) > 1.0e6
error('Simulation diverged or overflowed.');
end
end
y(end) = x(end) + cfg.noiseStd * randn();
gain(end) = nussbaum(kappa(end));
uCommand(end) = gain(end) * y(end);
uApplied(end) = min(max(uCommand(end), -cfg.uMax), cfg.uMax);
saturation(end) = abs(uApplied(end) - uCommand(end)) > 1.0e-12;
trip(end) = tripped;
result = struct('t', t, 'x', x, 'y', y, 'kappa', kappa, ...
'gain', gain, 'uCommand', uCommand, 'uApplied', uApplied, ...
'saturation', double(saturation), 'trip', double(trip));
end
function value = nussbaum(kappa)
value = kappa.^2 .* cos(kappa);
end
function printMetrics(label, result)
fprintf(['%s: final_x=% .6f, max_abs_x=% .6f, final_kappa=% .6f, ' ...
'max_abs_u=% .6f, sat_fraction=% .4f, tripped=%s\n'], ...
label, result.x(end), max(abs(result.x)), result.kappa(end), ...
max(abs(result.uApplied)), mean(result.saturation), ...
string(logical(result.trip(end))));
end
function buildNussbaumSimulinkModel(cfg, b)
model = 'Chapter16_Lesson4_Simulink';
if bdIsLoaded(model)
close_system(model, 0);
end
if exist([model '.slx'], 'file')
delete([model '.slx']);
end
new_system(model);
open_system(model);
add_block('simulink/Continuous/Integrator', [model '/Plant state x'], ...
'InitialCondition', num2str(cfg.x0), ...
'Position', [650 120 680 150]);
add_block('simulink/Math Operations/Gain', [model '/Plant drift a'], ...
'Gain', num2str(cfg.a), ...
'Position', [440 80 500 110]);
add_block('simulink/Math Operations/Gain', [model '/Unknown gain b'], ...
'Gain', num2str(b), ...
'Position', [440 175 500 205]);
add_block('simulink/Math Operations/Sum', [model '/Plant sum'], ...
'Inputs', '++', ...
'Position', [560 115 590 165]);
add_block('simulink/Continuous/Integrator', [model '/Kappa integrator'], ...
'InitialCondition', num2str(cfg.kappa0), ...
'Position', [265 285 295 315]);
add_block('simulink/User-Defined Functions/Fcn', [model '/Nussbaum map'], ...
'Expr', 'u(1)^2*cos(u(1))', ...
'Position', [335 270 445 310]);
add_block('simulink/Math Operations/Product', [model '/Control product'], ...
'Inputs', '**', ...
'Position', [335 145 380 190]);
add_block('simulink/Discontinuities/Saturation', [model '/Actuator saturation'], ...
'UpperLimit', num2str(cfg.uMax), ...
'LowerLimit', num2str(-cfg.uMax), ...
'Position', [395 145 425 190]);
add_block('simulink/Discontinuities/Dead Zone', [model '/Adaptation dead zone'], ...
'LowerValue', num2str(-cfg.deadZone), ...
'UpperValue', num2str(cfg.deadZone), ...
'Position', [70 260 140 305]);
add_block('simulink/Math Operations/Product', [model '/Squared excess error'], ...
'Inputs', '**', ...
'Position', [165 260 205 305]);
add_block('simulink/Math Operations/Gain', [model '/Adaptation gain gamma'], ...
'Gain', num2str(cfg.gamma), ...
'Position', [215 265 255 300]);
add_block('simulink/Signal Routing/Mux', [model '/Monitor mux'], ...
'Inputs', '3', ...
'Position', [735 100 740 220]);
add_block('simulink/Sinks/Scope', [model '/Scope'], ...
'Position', [790 125 850 185]);
add_line(model, 'Plant state x/1', 'Plant drift a/1');
add_line(model, 'Plant drift a/1', 'Plant sum/1');
add_line(model, 'Unknown gain b/1', 'Plant sum/2');
add_line(model, 'Plant sum/1', 'Plant state x/1');
add_line(model, 'Plant state x/1', 'Control product/1', 'autorouting', 'on');
add_line(model, 'Kappa integrator/1', 'Nussbaum map/1');
add_line(model, 'Nussbaum map/1', 'Control product/2');
add_line(model, 'Control product/1', 'Actuator saturation/1');
add_line(model, 'Actuator saturation/1', 'Unknown gain b/1');
add_line(model, 'Plant state x/1', 'Adaptation dead zone/1', 'autorouting', 'on');
add_line(model, 'Adaptation dead zone/1', 'Squared excess error/1');
add_line(model, 'Adaptation dead zone/1', 'Squared excess error/2');
add_line(model, 'Squared excess error/1', 'Adaptation gain gamma/1');
add_line(model, 'Adaptation gain gamma/1', 'Kappa integrator/1');
add_line(model, 'Plant state x/1', 'Monitor mux/1', 'autorouting', 'on');
add_line(model, 'Kappa integrator/1', 'Monitor mux/2', 'autorouting', 'on');
add_line(model, 'Actuator saturation/1', 'Monitor mux/3', 'autorouting', 'on');
add_line(model, 'Monitor mux/1', 'Scope/1');
set_param(model, 'Solver', 'ode4', ...
'SolverType', 'Fixed-step', ...
'FixedStep', num2str(cfg.dt), ...
'StopTime', num2str(cfg.duration));
save_system(model);
fprintf('Created %s.slx\n', model);
end
17. Wolfram Mathematica Implementation
The notebook uses built-in associations, random distributions, CSV export, datasets, and line plots. It performs the same two-sign experiment as the other implementations.
Chapter16_Lesson4.nb
Notebook[{
Cell["Chapter 16, Lesson 4: Practical Nussbaum-Gain Implementation", "Title"],
Cell["This notebook simulates a scalar plant with an unknown high-frequency gain sign. Measurement noise, actuator saturation, an adaptation dead zone, and a supervisory trip are included. These engineering safeguards modify the ideal continuous-time theorem and require separate analysis before hardware deployment.", "Text"],
Cell["ClearAll[\"Global`*\"];
config = <|\"dt\" -> 0.001, \"duration\" -> 20.0, \"a\" -> 0.25, \"gamma\" -> 0.20, \"kappa0\" -> 0.50,
\"x0\" -> 1.50, \"noiseStd\" -> 0.01, \"deadZone\" -> 0.03, \"uMax\" -> 8.0, \"kappaTrip\" -> 40.0, \"seed\" -> 7|>;
nussbaum[k_?NumericQ] := k^2 Cos[k]; saturate[value_?NumericQ, limit_?NumericQ] := Clip[value, {-limit, limit}];
finiteRealQ[value_] := NumberQ[value] && Im[N[value]] == 0 && Abs[N[value]] < Infinity;
simulate[b_?NumericQ, cfg_Association] := Module[
{dt, duration, steps, t, x, y, kappa, gain, uCommand, uApplied, saturation, trip, tripped = False,
error, excessError, kappaDot, disturbance, xDot, i},
If[Abs[b] < 10^-12, Message[simulate::bzero]; Return[$Failed]];
dt = cfg[\"dt\"]; duration = cfg[\"duration\"]; steps = Round[duration/dt] + 1;
t = N@Subdivide[0.0, duration, steps - 1];
x = ConstantArray[0.0, steps]; y = ConstantArray[0.0, steps]; kappa = ConstantArray[0.0, steps];
gain = ConstantArray[0.0, steps]; uCommand = ConstantArray[0.0, steps]; uApplied = ConstantArray[0.0, steps];
saturation = ConstantArray[0, steps]; trip = ConstantArray[0, steps];
x[[1]] = cfg[\"x0\"]; kappa[[1]] = cfg[\"kappa0\"]; SeedRandom[cfg[\"seed\"]];
Do[
y[[i]] = x[[i]] + cfg[\"noiseStd\"] RandomVariate[NormalDistribution[]];
error = y[[i]]; gain[[i]] = nussbaum[kappa[[i]]]; uCommand[[i]] = gain[[i]] error;
uApplied[[i]] = saturate[uCommand[[i]], cfg[\"uMax\"]];
saturation[[i]] = Boole[Abs[uApplied[[i]] - uCommand[[i]]] > 10^-12];
excessError = Max[Abs[error] - cfg[\"deadZone\"], 0.0]; kappaDot = cfg[\"gamma\"] excessError^2;
If[kappa[[i]] >= cfg[\"kappaTrip\"] || Not[finiteRealQ[gain[[i]]]], tripped = True];
If[tripped, uApplied[[i]] = 0.0; kappaDot = 0.0; trip[[i]] = 1];
disturbance = 0.02 Sin[1.4 t[[i]]]; xDot = cfg[\"a\"] x[[i]] + b uApplied[[i]] + disturbance;
(* Explicit Euler is intentionally retained to expose sample-time sensitivity. *)
x[[i + 1]] = x[[i]] + dt xDot; kappa[[i + 1]] = kappa[[i]] + dt kappaDot;
If[Not[finiteRealQ[x[[i + 1]]]] || Abs[x[[i + 1]]] > 10^6,
Print[\"Simulation diverged or overflowed.\"]; Return[$Failed]], {i, 1, steps - 1}];
y[[-1]] = x[[-1]] + cfg[\"noiseStd\"] RandomVariate[NormalDistribution[]];
gain[[-1]] = nussbaum[kappa[[-1]]]; uCommand[[-1]] = gain[[-1]] y[[-1]];
uApplied[[-1]] = saturate[uCommand[[-1]], cfg[\"uMax\"]];
saturation[[-1]] = Boole[Abs[uApplied[[-1]] - uCommand[[-1]]] > 10^-12]; trip[[-1]] = Boole[tripped];
<|\"t\" -> t, \"x\" -> x, \"y\" -> y, \"kappa\" -> kappa, \"gain\" -> gain, \"uCommand\" -> uCommand,
\"uApplied\" -> uApplied, \"saturation\" -> saturation, \"trip\" -> trip|>];
simulate::bzero = \"The control coefficient b must be nonzero.\"; metrics[label_String, result_Association] := <|\"case\" -> label, \"finalX\" -> Last[result[\"x\"]],
\"maxAbsX\" -> Max[Abs[result[\"x\"]]], \"finalKappa\" -> Last[result[\"kappa\"]],
\"maxAbsU\" -> Max[Abs[result[\"uApplied\"]]], \"saturationFraction\" -> Mean[result[\"saturation\"]],
\"tripped\" -> TrueQ[Last[result[\"trip\"]] == 1]|>;
exportCsv[label_String, result_Association] := Module[{header, rows},
header = {\"t\", \"x\", \"y\", \"kappa\", \"gain\", \"u_cmd\", \"u\", \"saturation\", \"trip\"};
rows = Transpose[Lookup[result, {\"t\", \"x\", \"y\", \"kappa\", \"gain\", \"uCommand\", \"uApplied\", \"saturation\", \"trip\"}]];
Export[\"Chapter16_Lesson4_\" <> label <> \".csv\", Prepend[rows, header], \"CSV\"]];
results = <|\"b_positive\" -> simulate[1.5, config], \"b_negative\" -> simulate[-1.5, config]|>;
Dataset[MapThread[metrics, {Keys[results], Values[results]}]]
KeyValueMap[exportCsv, results];
statePlots = KeyValueMap[ListLinePlot[{Transpose[{#2[\"t\"], #2[\"x\"]}], Transpose[{#2[\"t\"], #2[\"y\"]}]},
PlotLegends -> {\"state x\", \"measured y\"}, AxesLabel -> {\"time [s]\", \"state\"},
PlotLabel -> \"State response: \" <> #1, GridLines -> Automatic, ImageSize -> Large] &, results];
searchPlots = KeyValueMap[ListLinePlot[{Transpose[{#2[\"t\"], #2[\"kappa\"]}], Transpose[{#2[\"t\"], #2[\"gain\"]}]},
PlotLegends -> {\"kappa\", \"N(kappa)\"}, AxesLabel -> {\"time [s]\", \"adaptive variables\"},
PlotLabel -> \"Nussbaum search: \" <> #1, GridLines -> Automatic, ImageSize -> Large] &, results];
actuatorPlots = KeyValueMap[ListLinePlot[
{Transpose[{#2[\"t\"], #2[\"uCommand\"]}], Transpose[{#2[\"t\"], #2[\"uApplied\"]}]},
PlotLegends -> {\"commanded input\", \"applied input\"}, AxesLabel -> {\"time [s]\", \"control input\"},
PlotLabel -> \"Actuator effect: \" <> #1, GridLines -> Automatic, ImageSize -> Large] &, results];
Column[Join[statePlots, searchPlots, actuatorPlots]]", "Input"]
}, WindowTitle -> "Chapter16_Lesson4", StyleDefinitions -> "Default.nb"]
18. Problems and Solutions
Problem 1 (Noise-Induced Search Drift): Let the true tracking error be zero and let the measured error be \(y=n\), where \(n\) is zero-mean white measurement noise with variance \(\sigma_n^2\). For \(\dot\kappa=\gamma y^2\), find \(\mathbb{E}[\kappa(t)]\) when \(\kappa(0)=\kappa_0\).
Solution:
\[ \mathbb{E}[\dot\kappa]=\gamma\mathbb{E}[n^2] =\gamma\sigma_n^2. \]
Integrating the constant expected drift gives
\[ \mathbb{E}[\kappa(t)]=\kappa_0+\gamma\sigma_n^2 t. \]
Thus the expected search variable grows linearly even though the true error is zero. This explains why a noise floor can eventually move the controller away from a favorable Nussbaum interval.
Problem 2 (Saturation Defect Bound): For \(\dot x=b(u_{\mathrm{cmd}}+\Delta u)\), with \(u_{\mathrm{cmd}}=N(\kappa)x\) and \(\dot\kappa=x^2\), derive a bound on the additional Lyapunov term caused by \(\Delta u\).
Solution: For \(V=\tfrac12x^2\),
\[ \dot V=bN(\kappa)\dot\kappa+b x\Delta u. \]
By Young's inequality, for any \(\rho>0\),
\[ b x\Delta u\le |b x\Delta u| \le \frac{\rho}{2}x^2+\frac{b^2}{2\rho}(\Delta u)^2. \]
Persistent saturation produces a nonvanishing disturbance term. The ideal contradiction proof does not establish asymptotic regulation unless further conditions make this defect integrable or otherwise dominated.
Problem 3 (Euler Sample-Time Restriction): During one sample, suppose \(\lambda=a+bN(\kappa)\) is constant and negative. Determine the explicit-Euler stability interval.
Solution:
\[ x_{k+1}=(1+h\lambda)x_k. \]
Discrete stability requires
\[ |1+h\lambda|<1. \]
For real negative \(\lambda\), this is equivalent to
\[ -2<h\lambda<0 \qquad\Longleftrightarrow\qquad 0<h<\frac{2}{|\lambda|}. \]
As the Nussbaum magnitude grows, the admissible sample period shrinks. A fixed sample period that is adequate near the origin may fail later in the search.
Problem 4 (Unknown Constant Sign vs Sign Switching): Explain why the standard Nussbaum proof for constant \(b\) does not directly cover a coefficient \(b(t)\) that changes sign.
Solution:
For constant \(b\), the Lyapunov identity contains \(b\int N(s)ds\), so the fixed sign of \(b\) selects which oscillatory subsequence yields the contradiction. For time-varying \(b(t)\), one obtains \(\int b(t)N(\kappa(t))\dot\kappa(t)dt\), which cannot in general be converted to a fixed multiple of the primitive of \(N\). A sign crossing also creates an instant of zero control authority. Additional assumptions and a stronger lemma are required.
Problem 5 (Numerical Envelope): Suppose \(N(\kappa)=\kappa^2\cos\kappa\), the normalized error is bounded by \(|e_s|\le e_{\max}\), and the numerical command should satisfy \(|u_{\mathrm{cmd}}|\le u_{\mathrm{num}}\). Derive a conservative bound on \(|\kappa|\).
Solution: Since \(|\cos\kappa|\le1\),
\[ |u_{\mathrm{cmd}}|=|\kappa^2\cos\kappa\,e_s| \le \kappa^2e_{\max}. \]
It is sufficient to require
\[ |\kappa|\le \sqrt{\frac{u_{\mathrm{num}}}{e_{\max}}}. \]
This is a numerical or supervisory envelope, not a theorem-preserving projection. Reaching the envelope should trigger a documented safe mode rather than silent clipping.
19. Summary
Nussbaum-gain control resolves an unknown constant control direction through an oscillatory integral argument, but practical implementation changes the closed-loop equations. Saturation introduces an unmatched input defect; measurement noise drives the monotone search variable; sampling can turn a stable continuous-time interval into an unstable discrete multiplier; delay and unmodeled dynamics create phase errors amplified by the adaptive gain; and sign-changing control effectiveness lies outside the standard theorem.
Reliable implementation therefore combines normalization, moderate-growth functions, explicit actuator models, finite-value checks, logging, staged worst-case validation, and an independent safety path. Every dead zone, leakage term, freeze, reset, clamp, or fallback must be treated as a change to the controller and to its formal stability claim.
20. References
- Nussbaum, R.D. (1983). Some remarks on a conjecture in parameter adaptive control. Systems & Control Letters, 3(5), 243–246. doi:10.1016/0167-6911(83)90021-X.
- 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.
- 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.
- Mårtensson, B. (1990). Remarks on adaptive stabilization of first-order nonlinear systems. Systems & Control Letters, 14(1), 1–7.
- Ryan, E.P. (1991). A universal adaptive stabilizer for a class of nonlinear systems. Systems & Control Letters, 16(4), 209–218.
- Kaloust, J., & Qu, Z. (1995). Continuous robust control design for nonlinear uncertain systems without a priori knowledge of control direction. IEEE Transactions on Automatic Control, 40(2), 276–282.
- Ye, X., & Jiang, J. (1998). Adaptive nonlinear design without a priori knowledge of control directions. IEEE Transactions on Automatic Control, 43(11), 1617–1621. doi:10.1109/9.728882.
- Ding, Z. (1998). Global adaptive output-feedback stabilization of nonlinear systems of any relative degree with unknown high-frequency gains. IEEE Transactions on Automatic Control, 43(10), 1442–1446. doi:10.1109/9.720504.
- Ye, X. (1999). Asymptotic regulation of time-varying uncertain nonlinear systems with unknown control directions. Automatica, 35(5), 929–935.
- Ge, S.S., & Wang, J. (2003). Robust adaptive tracking for time-varying uncertain nonlinear systems with unknown control coefficients. IEEE Transactions on Automatic Control, 48(8), 1463–1469. doi:10.1109/TAC.2003.815049.
- Chen, Z. (2019). Nussbaum functions in adaptive control with time-varying unknown control coefficients. Automatica, 102, 72–79. doi:10.1016/j.automatica.2018.12.035.
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.