Chapter 15: Output-Feedback Adaptive Control – Basic Ideas

Lesson 1: When Full State is Not Measured – Problem Setup

This lesson formulates the adaptive-control problem when the controller has access only to measured outputs rather than the complete plant state. We distinguish physical state, measured information, controller memory, and unknown parameters; derive the observability and input-output conditions that make output feedback plausible; and show why directly replacing a state by an output or by a noisy numerical derivative does not preserve the stability arguments developed for state-feedback adaptive control.

1. The Change in Information Pattern

Earlier state-feedback adaptive designs assumed that every component of \( \mathbf{x}(t) \in \mathbb{R}^{n} \) was available to the controller. In many physical systems, sensors provide only \( p \) outputs with \( p < n \). The plant may therefore be represented as

\[ \dot{\mathbf{x} } = \mathbf{A}(\boldsymbol{\theta})\mathbf{x} + \mathbf{B}(\boldsymbol{\theta})u + \mathbf{d}(t), \qquad y = \mathbf{C}\mathbf{x} + v(t), \]

where \( \boldsymbol{\theta} \) is an unknown constant parameter vector, \( \mathbf{d}(t) \) is an unmodeled disturbance, and \( v(t) \) is measurement noise. The controller receives \( r(t),y(t) \) and its own past input, but it does not receive \( \mathbf{x}(t) \).

flowchart TD
  R["Reference r"] --> C["Dynamic adaptive controller"]
  Y["Measured output y"] --> C
  C -->|"control u"| P["Uncertain plant"]
  P -->|"physical state x: not directly available"| S["Sensors"]
  S -->|"partial measurement y"| Y
  C --> M["Controller memory, filters, \nparameter estimates"]
  M --> C
        

The central issue is not merely that fewer numbers are measured. The adaptive law and control law must now be realizable causal mappings of the available signal history \( \mathcal{I}_t = \{r(\tau),y(\tau),u(\tau):0\leq\tau\leq t\} \).

2. Full-State Feedback Versus Output Feedback

A state-feedback MRAC or direct adaptive controller commonly has the parameterized form

\[ u = \hat{\mathbf{k} }_x^{T}\mathbf{x} + \hat{k}_r r, \qquad \dot{\hat{\mathbf{k} } }_x = -\boldsymbol{\Gamma}_x\mathbf{x}e^{T}\mathbf{P}\mathbf{b}_m. \]

Both equations explicitly require \( \mathbf{x} \). When only \( y=\mathbf{C}\mathbf{x} \) is measured, the substitution \( \mathbf{x}\leftarrow y \) is generally dimensionally invalid and, even when dimensions happen to agree, changes the closed-loop equations on which the Lyapunov cancellation was based.

An output-feedback adaptive controller must instead be dynamic:

\[ \begin{aligned} \dot{\boldsymbol{\xi} } &= \mathbf{f}_{c}(\boldsymbol{\xi},y,r,\hat{\boldsymbol{\theta} }),\\ u &= h_c(\boldsymbol{\xi},y,r,\hat{\boldsymbol{\theta} }),\\ \dot{\hat{\boldsymbol{\theta} } } &= \mathcal{A}(\boldsymbol{\xi},y,r,u), \end{aligned} \]

where \( \boldsymbol{\xi} \) stores filter states, controller states, or state-estimation variables. Lesson 2 will construct simple observer structures; here we identify the conditions and coupling terms that any such construction must address.

3. Observability: Can the Missing State Be Inferred?

For a known linear pair \( (\mathbf{A},\mathbf{C}) \), the observability matrix is

\[ \mathcal{O} = \begin{bmatrix} \mathbf{C}\\ \mathbf{C}\mathbf{A}\\ \vdots\\ \mathbf{C}\mathbf{A}^{n-1} \end{bmatrix}. \]

The pair is observable when \( \operatorname{rank}(\mathcal{O})=n \). Observability means that, with the model known, the initial state can be uniquely reconstructed from a finite interval of input-output data. It does not mean that the state is instantaneously measured, nor does it by itself solve the problem when \( \mathbf{A} \) is uncertain.

Proposition 1: Non-injective output maps lose instantaneous state information

Suppose \( \mathbf{C}\in\mathbb{R}^{p\times n} \) has \( \operatorname{rank}(\mathbf{C}) < n \). Then no static function \( \psi:\mathbb{R}^{p}\rightarrow\mathbb{R}^{n} \) can satisfy \( \psi(\mathbf{C}\mathbf{x})=\mathbf{x} \) for every \( \mathbf{x}\in\mathbb{R}^{n} \).

Proof. Since the null space of \( \mathbf{C} \) is nontrivial, choose \( \mathbf{q}\neq\mathbf{0} \) with \( \mathbf{Cq}=\mathbf{0} \). For any \( \mathbf{x} \), the distinct states \( \mathbf{x} \) and \( \mathbf{x}+\mathbf{q} \) produce the same output. A single-valued static map cannot return both states from that identical output. Therefore reconstruction requires dynamics and signal history. □

Derivative interpretation

In the noise-free known-model case, \( y,\dot y,\ldots,y^{(n-1)} \) contain the rows of \( \mathcal{O} \) multiplying the state, plus known input terms. This explains the rank condition, but direct numerical differentiation is usually unsuitable because it amplifies measurement noise and can create noncausal or high-gain behavior.

4. A Second-Order Running Example

Consider an uncertain second-order plant with position measured and velocity unmeasured:

\[ \dot{x}_1=x_2,\qquad \dot{x}_2=-a_0x_1-a_1x_2+bu,\qquad y=x_1, \]

where \( a_0,a_1,b \) are constant but uncertain. The nominal observability matrix is

\[ \mathcal{O}=\begin{bmatrix}1&0\\0&1\end{bmatrix}, \qquad \operatorname{rank}(\mathcal{O})=2. \]

Thus velocity is inferable from output history, although it is not directly available. A state-feedback law \( u=-k_1x_1-k_2x_2+k_rr \) cannot be implemented as written. Replacing \( x_2 \) by \( \dot y \) gives the formal identity \( x_2=\dot y \) only in the ideal noise-free model. With measured output \( y_m=y+v \), differentiation gives

\[ \frac{d y_m}{dt}=x_2+\dot v, \]

so even small high-frequency noise can dominate the derivative. Stable filters or observers are therefore structural components rather than optional numerical conveniences.

5. Input-Output Structure: Relative Degree and Hidden Dynamics

For the known linear system \( \dot{\mathbf{x} }=\mathbf{A}\mathbf{x}+\mathbf{B}u \), \( y=\mathbf{C}\mathbf{x} \), the relative degree \( \rho \) is the smallest positive integer satisfying

\[ \mathbf{C}\mathbf{A}^{\rho-1}\mathbf{B}\neq 0, \qquad \mathbf{C}\mathbf{A}^{i}\mathbf{B}=0, \quad i=0,1,\ldots,\rho-2. \]

The input first appears in \( y^{(\rho)} \). An adaptive law must respect this input-output phase relationship. A design derived for relative degree one generally cannot be copied to a relative-degree-two plant by changing gains only.

If the transfer function is \( G(s)=k_p Z(s)/R(s) \), the roots of \( Z(s) \) are transmission zeros. Output tracking can force internal modes associated with these zeros. Stable inversion-based adaptive tracking therefore normally requires a minimum-phase plant, or a design that explicitly accommodates nonminimum-phase behavior.

Why observability is not enough

Observability concerns reconstruction of state from input-output history. Minimum phase concerns stability of internal dynamics under output regulation. A plant can be observable yet nonminimum phase; in that case state information may be reconstructible while aggressive output tracking still excites unstable hidden dynamics.

6. Constructing Measurable Regressors with Stable Filters

Output-feedback adaptive control often replaces unavailable state regressors by filtered input-output regressors. Consider the SISO model

\[ y^{(n)}+a_{n-1}y^{(n-1)}+\cdots+a_0y =b_m u^{(m)}+\cdots+b_0u, \qquad m=n-\rho. \]

Choose a known monic Hurwitz polynomial \( \Lambda(s) \) of degree \( n \) and apply the proper stable filters \( s^i/\Lambda(s) \) to measured signals. Define

\[ z_{y,i}=\frac{s^i}{\Lambda(s)}y,\quad i=0,\ldots,n-1, \qquad z_{u,j}=\frac{s^j}{\Lambda(s)}u,\quad j=0,\ldots,m. \]

After filtering and collecting known terms, the plant admits a measurable regression of the conceptual form

\[ y = \boldsymbol{\theta}^{T}\boldsymbol{\phi}_f + \varepsilon_f, \qquad \boldsymbol{\phi}_f= \begin{bmatrix}z_{y,0}&\cdots&z_{y,n-1}& z_{u,0}&\cdots&z_{u,m}\end{bmatrix}^{T}, \]

where \( \varepsilon_f \) is an exponentially decaying term due to filter initial conditions in the exact noise-free model. Unlike raw derivatives, these signals are generated by causal stable dynamics. However, filter bandwidth creates a trade-off: faster filters reduce phase lag but amplify noise and unmodeled high-frequency dynamics.

Realizability condition

Every signal in the control and update laws must be computable from \( \mathcal{I}_t \). A Lyapunov formula containing an unmeasured state may prove stability of an ideal mathematical controller, but it does not define an implementable output-feedback controller.

7. Why the Classical Separation Principle Does Not Transfer Automatically

For a known linear plant, a fixed state-feedback controller and a fixed observer can often be designed separately. In an adaptive loop, the controller parameters and possibly the observer parameters evolve online. Let \( \tilde{\mathbf{x} }=\mathbf{x}-\hat{\mathbf{x} } \) and \( \tilde{\boldsymbol{\theta} }= \boldsymbol{\theta}-\hat{\boldsymbol{\theta} } \). If the implemented control uses \( \hat{\mathbf{x} } \), then

\[ u=\hat{\mathbf{k} }_x^{T}\hat{\mathbf{x} }+\hat{k}_r r =\hat{\mathbf{k} }_x^{T}\mathbf{x} -\hat{\mathbf{k} }_x^{T}\tilde{\mathbf{x} }+\hat{k}_r r. \]

The extra term \( -\hat{\mathbf{k} }_x^{T}\tilde{\mathbf{x} } \) enters the tracking-error dynamics, while parameter errors enter the state-estimation dynamics. A schematic coupled model is

\[ \begin{aligned} \dot{\mathbf{e} } &= \mathbf{A}_m\mathbf{e} +\mathbf{B}_e\tilde{\boldsymbol{\theta} }^{T}\boldsymbol{\phi} +\mathbf{D}_e(\hat{\boldsymbol{\theta} })\tilde{\mathbf{x} },\\ \dot{\tilde{\mathbf{x} } } &= \mathbf{A}_o(\hat{\boldsymbol{\theta} })\tilde{\mathbf{x} } +\mathbf{D}_x(\mathbf{x},u)\tilde{\boldsymbol{\theta} },\\ \dot{\tilde{\boldsymbol{\theta} } } &= -\mathcal{A}(\mathbf{e},\hat{\mathbf{x} },y,u). \end{aligned} \]

flowchart TD
  E["Tracking error e"] --> A["Parameter update law"]
  XH["Estimated or filtered state information"] --> A
  A --> TH["Changing parameter estimates"]
  TH --> C["Changing controller dynamics"]
  TH --> O["Changing estimation dynamics"]
  O --> XT["State-estimation error"]
  XT --> C
  C --> E
        

A composite Lyapunov candidate may need all three error energies,

\[ V=\mathbf{e}^{T}\mathbf{P}\mathbf{e} +\tilde{\boldsymbol{\theta} }^{T}\boldsymbol{\Gamma}^{-1} \tilde{\boldsymbol{\theta} } +\tilde{\mathbf{x} }^{T}\mathbf{S}\tilde{\mathbf{x} }, \]

and its derivative contains cross terms such as \( \mathbf{e}^{T}\mathbf{P}\mathbf{D}_e \tilde{\mathbf{x} } \) and \( \tilde{\mathbf{x} }^{T}\mathbf{S}\mathbf{D}_x \tilde{\boldsymbol{\theta} } \). These terms are the mathematical reason that an observer and an adaptive state-feedback law cannot simply be connected and assumed stable.

8. Problem Statement for Output-Feedback Adaptive Tracking

Given an uncertain plant and a stable reference model

\[ \dot{\mathbf{x} }_m=\mathbf{A}_m\mathbf{x}_m+\mathbf{B}_m r, \qquad y_m=\mathbf{C}_m\mathbf{x}_m, \]

construct a causal dynamic controller using only \( r,y,u \) and internal controller states such that:

  1. all closed-loop signals remain bounded;
  2. the output tracking error \( e_y=y-y_m \) converges to zero, or to a quantified neighborhood under disturbances;
  3. the controller does not require unknown plant parameters;
  4. the design respects known relative degree and control-direction assumptions;
  5. state reconstruction, filtering, and adaptation remain mutually stable.

Parameter convergence is a separate objective. As established in earlier chapters, tracking may be achieved without convergence of every parameter; convergence generally requires sufficient excitation of the measurable regressor.

Typical assumptions to state explicitly

  • The plant order or an upper bound is known.
  • The input-output relative degree is known.
  • The sign of the high-frequency gain is known in this chapter.
  • The plant is observable or detectable for admissible parameters.
  • The zero dynamics satisfy the stability requirement used by the design.
  • Reference and disturbance signals meet stated boundedness conditions.

9. A Filtered Velocity Proxy: What It Shows and What It Does Not Prove

For the running example, one elementary causal proxy is generated by

\[ \dot z=-\lambda z+\lambda y_m,\qquad \hat{x}_{2,f}=\lambda(y_m-z). \]

Its transfer function from measured output to the proxy is \( \lambda s/(s+\lambda) \). At frequencies well below \( \lambda \), it approximates differentiation; at high frequencies, its gain approaches \( \lambda \) instead of increasing without bound. Nevertheless, it is not a state observer: its error depends on plant acceleration, filter initial conditions, noise, and bandwidth. It is included in the implementations to expose the information constraint, not as a stability-certified adaptive design.

\[ \tilde{x}_{2,f}=x_2-\hat{x}_{2,f}, \qquad u_f=-k_p y_m-k_d\hat{x}_{2,f}+k_r r. \]

Comparing this branch with an unavailable full-state benchmark quantifies the performance cost and noise sensitivity introduced by partial measurement. The next lesson replaces this diagnostic proxy with a systematic Luenberger-type structure for adaptive use.

10. Python Implementation

The Python version uses NumPy for the observability-rank calculation and vector arithmetic. It simulates the full-state benchmark and the measurement-limited branch with the same uncertain plant.

Chapter15_Lesson1.py

# Chapter15_Lesson1.py
# Measurement-limited adaptive-control setup for a second-order SISO plant.
# Requires: numpy

import csv
import math
import numpy as np

DT = 0.001
T_FINAL = 12.0
N = int(T_FINAL / DT) + 1

# Unknown-to-the-controller plant parameters used by the simulator.
a0, a1, b = 2.0, 0.8, 1.3

# Reference model: y_m'' + 2*zeta*wn*y_m' + wn^2*y_m = wn^2*r.
wn, zeta = 1.4, 0.9

# Fixed benchmark gains. The full-state branch uses x2 directly; the
# output-feedback branch uses a filtered derivative proxy constructed from y.
kp, kd, kr = 3.0, 2.0, 3.0
filter_pole = 18.0
noise_amplitude = 0.01


def reference(t: float) -> float:
    return 1.0 if t >= 1.0 else 0.0


def plant_rhs(x: np.ndarray, u: float) -> np.ndarray:
    return np.array([x[1], -a0 * x[0] - a1 * x[1] + b * u], dtype=float)


def model_rhs(xm: np.ndarray, r: float) -> np.ndarray:
    return np.array(
        [xm[1], -(wn**2) * xm[0] - 2.0 * zeta * wn * xm[1] + (wn**2) * r],
        dtype=float,
    )


def rk4_step(rhs, state: np.ndarray, dt: float, *args) -> np.ndarray:
    k1 = rhs(state, *args)
    k2 = rhs(state + 0.5 * dt * k1, *args)
    k3 = rhs(state + 0.5 * dt * k2, *args)
    k4 = rhs(state + dt * k3, *args)
    return state + (dt / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4)


# For C = [1, 0], O = [C; C A]. Rank(O) = 2, so the plant is observable.
A = np.array([[0.0, 1.0], [-a0, -a1]])
C = np.array([[1.0, 0.0]])
O = np.vstack((C, C @ A))
print("Observability matrix:\n", O)
print("Observability rank:", np.linalg.matrix_rank(O))

x_full = np.array([0.0, 0.0])
x_output = np.array([0.0, 0.0])
xm = np.array([0.0, 0.0])
filtered_y = 0.0

rows = []
error_sq_full = 0.0
error_sq_output = 0.0
proxy_error_sq = 0.0

rng = np.random.default_rng(7)
for k in range(N):
    t = k * DT
    r = reference(t)
    ym = xm[0]

    # Idealized benchmark: x2 is used even though it is not measured.
    u_full = -kp * x_full[0] - kd * x_full[1] + kr * r

    # Available measurement and stable first-order filter.
    y_meas = x_output[0] + noise_amplitude * rng.standard_normal()
    filtered_y += DT * (-filter_pole * filtered_y + filter_pole * y_meas)
    x2_proxy = filter_pole * (y_meas - filtered_y)
    u_output = -kp * y_meas - kd * x2_proxy + kr * r

    rows.append(
        [
            t,
            r,
            ym,
            x_full[0],
            x_full[1],
            u_full,
            y_meas,
            x_output[1],
            x2_proxy,
            u_output,
        ]
    )

    error_sq_full += (x_full[0] - ym) ** 2
    error_sq_output += (x_output[0] - ym) ** 2
    proxy_error_sq += (x2_proxy - x_output[1]) ** 2

    x_full = rk4_step(plant_rhs, x_full, DT, u_full)
    x_output = rk4_step(plant_rhs, x_output, DT, u_output)
    xm = rk4_step(model_rhs, xm, DT, r)

with open("Chapter15_Lesson1_results.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(
        [
            "time",
            "reference",
            "reference_output",
            "full_state_output",
            "full_state_velocity",
            "full_state_control",
            "measured_output",
            "hidden_velocity",
            "velocity_proxy",
            "output_feedback_control",
        ]
    )
    writer.writerows(rows)

print(f"Full-state benchmark RMSE: {math.sqrt(error_sq_full / N):.6f}")
print(f"Output-feedback RMSE:      {math.sqrt(error_sq_output / N):.6f}")
print(f"Velocity-proxy RMSE:       {math.sqrt(proxy_error_sq / N):.6f}")
print("Saved Chapter15_Lesson1_results.csv")

11. C++ Implementation

This dependency-free C++17 implementation uses fixed-size structures and a from-scratch RK4 integrator. In larger projects, Eigen is a suitable library for observability matrices and higher-order state-space models.

Chapter15_Lesson1.cpp

// Chapter15_Lesson1.cpp
// Measurement-limited adaptive-control setup for a second-order SISO plant.
// Build: g++ -std=c++17 -O2 Chapter15_Lesson1.cpp -o Chapter15_Lesson1

#include <array>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <random>

struct State {
    double x1{};
    double x2{};
};

constexpr double dt = 0.001;
constexpr double tFinal = 12.0;
constexpr double a0 = 2.0;
constexpr double a1 = 0.8;
constexpr double b = 1.3;
constexpr double wn = 1.4;
constexpr double zeta = 0.9;
constexpr double kp = 3.0;
constexpr double kd = 2.0;
constexpr double kr = 3.0;
constexpr double filterPole = 18.0;
constexpr double noiseAmplitude = 0.01;

double reference(double t) { return t >= 1.0 ? 1.0 : 0.0; }

State plantRhs(const State& x, double u) {
    return {x.x2, -a0 * x.x1 - a1 * x.x2 + b * u};
}

State modelRhs(const State& xm, double r) {
    return {xm.x2, -wn * wn * xm.x1 - 2.0 * zeta * wn * xm.x2 + wn * wn * r};
}

State add(const State& x, const State& dx, double scale) {
    return {x.x1 + scale * dx.x1, x.x2 + scale * dx.x2};
}

template <typename Rhs>
State rk4Step(Rhs rhs, const State& x, double h, double input) {
    const State k1 = rhs(x, input);
    const State k2 = rhs(add(x, k1, 0.5 * h), input);
    const State k3 = rhs(add(x, k2, 0.5 * h), input);
    const State k4 = rhs(add(x, k3, h), input);
    return {
        x.x1 + h * (k1.x1 + 2.0 * k2.x1 + 2.0 * k3.x1 + k4.x1) / 6.0,
        x.x2 + h * (k1.x2 + 2.0 * k2.x2 + 2.0 * k3.x2 + k4.x2) / 6.0,
    };
}

int main() {
    // C = [1, 0], C*A = [0, 1], hence det(O) = 1 and rank(O) = 2.
    const std::array<std::array<double, 2>, 2> observability{ { {1.0, 0.0}, {0.0, 1.0} } };
    const double detO = observability[0][0] * observability[1][1] -
                        observability[0][1] * observability[1][0];
    std::cout << "det(O) = " << detO << ", observability rank = "
              << (std::abs(detO) > 1e-12 ? 2 : 1) << '\n';

    State xFull{};
    State xOutput{};
    State xm{};
    double filteredY = 0.0;
    double errorSqFull = 0.0;
    double errorSqOutput = 0.0;
    double proxyErrorSq = 0.0;

    std::mt19937 generator(7);
    std::normal_distribution<double> normal(0.0, 1.0);

    std::ofstream csv("Chapter15_Lesson1_results.csv");
    if (!csv) {
        std::cerr << "Cannot create results file.\n";
        return 1;
    }
    csv << "time,reference,reference_output,full_state_output,full_state_velocity,"
           "full_state_control,measured_output,hidden_velocity,velocity_proxy,"
           "output_feedback_control\n";
    csv << std::setprecision(12);

    const int steps = static_cast<int>(tFinal / dt) + 1;
    for (int k = 0; k < steps; ++k) {
        const double t = k * dt;
        const double r = reference(t);
        const double ym = xm.x1;

        const double uFull = -kp * xFull.x1 - kd * xFull.x2 + kr * r;
        const double yMeasured = xOutput.x1 + noiseAmplitude * normal(generator);
        filteredY += dt * (-filterPole * filteredY + filterPole * yMeasured);
        const double x2Proxy = filterPole * (yMeasured - filteredY);
        const double uOutput = -kp * yMeasured - kd * x2Proxy + kr * r;

        csv << t << ',' << r << ',' << ym << ',' << xFull.x1 << ',' << xFull.x2
            << ',' << uFull << ',' << yMeasured << ',' << xOutput.x2 << ','
            << x2Proxy << ',' << uOutput << '\n';

        errorSqFull += std::pow(xFull.x1 - ym, 2.0);
        errorSqOutput += std::pow(xOutput.x1 - ym, 2.0);
        proxyErrorSq += std::pow(x2Proxy - xOutput.x2, 2.0);

        xFull = rk4Step(plantRhs, xFull, dt, uFull);
        xOutput = rk4Step(plantRhs, xOutput, dt, uOutput);
        xm = rk4Step(modelRhs, xm, dt, r);
    }

    std::cout << std::fixed << std::setprecision(6);
    std::cout << "Full-state benchmark RMSE: " << std::sqrt(errorSqFull / steps) << '\n';
    std::cout << "Output-feedback RMSE:      " << std::sqrt(errorSqOutput / steps) << '\n';
    std::cout << "Velocity-proxy RMSE:       " << std::sqrt(proxyErrorSq / steps) << '\n';
    std::cout << "Saved Chapter15_Lesson1_results.csv\n";
    return 0;
}

12. Java Implementation

The Java implementation uses only the standard library. For general matrix operations, EJML or Apache Commons Math can replace the hand-coded second-order calculations.

Chapter15_Lesson1.java

// Chapter15_Lesson1.java
// Measurement-limited adaptive-control setup for a second-order SISO plant.
// Build and run: javac Chapter15_Lesson1.java && java Chapter15_Lesson1

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Locale;
import java.util.Random;

public final class Chapter15_Lesson1 {
    private static final double DT = 0.001;
    private static final double T_FINAL = 12.0;
    private static final double A0 = 2.0;
    private static final double A1 = 0.8;
    private static final double B = 1.3;
    private static final double WN = 1.4;
    private static final double ZETA = 0.9;
    private static final double KP = 3.0;
    private static final double KD = 2.0;
    private static final double KR = 3.0;
    private static final double FILTER_POLE = 18.0;
    private static final double NOISE_AMPLITUDE = 0.01;

    private static final class State {
        final double x1;
        final double x2;

        State(double x1, double x2) {
            this.x1 = x1;
            this.x2 = x2;
        }
    }

    private interface Dynamics {
        State rhs(State x, double input);
    }

    private static double reference(double t) {
        return t >= 1.0 ? 1.0 : 0.0;
    }

    private static State add(State x, State dx, double scale) {
        return new State(x.x1 + scale * dx.x1, x.x2 + scale * dx.x2);
    }

    private static State rk4Step(Dynamics dynamics, State x, double h, double input) {
        State k1 = dynamics.rhs(x, input);
        State k2 = dynamics.rhs(add(x, k1, 0.5 * h), input);
        State k3 = dynamics.rhs(add(x, k2, 0.5 * h), input);
        State k4 = dynamics.rhs(add(x, k3, h), input);
        return new State(
            x.x1 + h * (k1.x1 + 2.0 * k2.x1 + 2.0 * k3.x1 + k4.x1) / 6.0,
            x.x2 + h * (k1.x2 + 2.0 * k2.x2 + 2.0 * k3.x2 + k4.x2) / 6.0
        );
    }

    public static void main(String[] args) throws IOException {
        Locale.setDefault(Locale.US);

        // O = [[1, 0], [0, 1]], so det(O) = 1 and rank(O) = 2.
        double detO = 1.0;
        System.out.printf("det(O) = %.1f, observability rank = %d%n", detO, 2);

        Dynamics plant = (x, u) -> new State(x.x2, -A0 * x.x1 - A1 * x.x2 + B * u);
        Dynamics model = (xm, r) -> new State(
            xm.x2,
            -WN * WN * xm.x1 - 2.0 * ZETA * WN * xm.x2 + WN * WN * r
        );

        State xFull = new State(0.0, 0.0);
        State xOutput = new State(0.0, 0.0);
        State xm = new State(0.0, 0.0);
        double filteredY = 0.0;
        double errorSqFull = 0.0;
        double errorSqOutput = 0.0;
        double proxyErrorSq = 0.0;
        Random random = new Random(7);

        int steps = (int) (T_FINAL / DT) + 1;
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("Chapter15_Lesson1_results.csv"))) {
            writer.write("time,reference,reference_output,full_state_output,full_state_velocity,");
            writer.write("full_state_control,measured_output,hidden_velocity,velocity_proxy,");
            writer.write("output_feedback_control\n");

            for (int k = 0; k < steps; ++k) {
                double t = k * DT;
                double r = reference(t);
                double ym = xm.x1;

                double uFull = -KP * xFull.x1 - KD * xFull.x2 + KR * r;
                double yMeasured = xOutput.x1 + NOISE_AMPLITUDE * random.nextGaussian();
                filteredY += DT * (-FILTER_POLE * filteredY + FILTER_POLE * yMeasured);
                double x2Proxy = FILTER_POLE * (yMeasured - filteredY);
                double uOutput = -KP * yMeasured - KD * x2Proxy + KR * r;

                writer.write(String.format(
                    Locale.US,
                    "%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f,%.12f%n",
                    t, r, ym, xFull.x1, xFull.x2, uFull, yMeasured,
                    xOutput.x2, x2Proxy, uOutput
                ));

                errorSqFull += Math.pow(xFull.x1 - ym, 2.0);
                errorSqOutput += Math.pow(xOutput.x1 - ym, 2.0);
                proxyErrorSq += Math.pow(x2Proxy - xOutput.x2, 2.0);

                xFull = rk4Step(plant, xFull, DT, uFull);
                xOutput = rk4Step(plant, xOutput, DT, uOutput);
                xm = rk4Step(model, xm, DT, r);
            }
        }

        System.out.printf("Full-state benchmark RMSE: %.6f%n", Math.sqrt(errorSqFull / steps));
        System.out.printf("Output-feedback RMSE:      %.6f%n", Math.sqrt(errorSqOutput / steps));
        System.out.printf("Velocity-proxy RMSE:       %.6f%n", Math.sqrt(proxyErrorSq / steps));
        System.out.println("Saved Chapter15_Lesson1_results.csv");
    }
}

13. MATLAB/Simulink Implementation

The script uses base MATLAB and computes the observability matrix explicitly. With Control System Toolbox, the equivalent general command is obsv(A,C). In Simulink, implement the plant and reference model with State-Space blocks, the filter with a Transfer Fcn block filterPole*s/(s+filterPole), and log the hidden simulated state only for validation—not as a controller input.

Chapter15_Lesson1.m

% Chapter15_Lesson1.m
% Measurement-limited adaptive-control setup for a second-order SISO plant.
% Uses only base MATLAB. Control System Toolbox is optional for obsv().

clear; clc;

dt = 0.001;
tFinal = 12.0;
t = (0:dt:tFinal)';
N = numel(t);

% Unknown-to-the-controller plant parameters used by the simulator.
a0 = 2.0; a1 = 0.8; b = 1.3;
wn = 1.4; zeta = 0.9;
kp = 3.0; kd = 2.0; kr = 3.0;
filterPole = 18.0;
noiseAmplitude = 0.01;
rng(7);

A = [0 1; -a0 -a1];
C = [1 0];
O = [C; C*A];
fprintf('Observability rank: %d\n', rank(O));

xFull = zeros(2,1);
xOutput = zeros(2,1);
xm = zeros(2,1);
filteredY = 0.0;

result = zeros(N,10);
errorSqFull = 0.0;
errorSqOutput = 0.0;
proxyErrorSq = 0.0;

for k = 1:N
    tk = t(k);
    r = double(tk >= 1.0);
    ym = xm(1);

    % Idealized benchmark: velocity x(2) is used despite not being measured.
    uFull = -kp*xFull(1) - kd*xFull(2) + kr*r;

    yMeasured = xOutput(1) + noiseAmplitude*randn;
    filteredY = filteredY + dt*(-filterPole*filteredY + filterPole*yMeasured);
    x2Proxy = filterPole*(yMeasured - filteredY);
    uOutput = -kp*yMeasured - kd*x2Proxy + kr*r;

    result(k,:) = [tk r ym xFull(1) xFull(2) uFull ...
                   yMeasured xOutput(2) x2Proxy uOutput];

    errorSqFull = errorSqFull + (xFull(1)-ym)^2;
    errorSqOutput = errorSqOutput + (xOutput(1)-ym)^2;
    proxyErrorSq = proxyErrorSq + (x2Proxy-xOutput(2))^2;

    xFull = rk4Plant(xFull,uFull,dt,a0,a1,b);
    xOutput = rk4Plant(xOutput,uOutput,dt,a0,a1,b);
    xm = rk4Model(xm,r,dt,wn,zeta);
end

names = {'time','reference','reference_output','full_state_output', ...
         'full_state_velocity','full_state_control','measured_output', ...
         'hidden_velocity','velocity_proxy','output_feedback_control'};
T = array2table(result,'VariableNames',names);
writetable(T,'Chapter15_Lesson1_results.csv');

fprintf('Full-state benchmark RMSE: %.6f\n',sqrt(errorSqFull/N));
fprintf('Output-feedback RMSE:      %.6f\n',sqrt(errorSqOutput/N));
fprintf('Velocity-proxy RMSE:       %.6f\n',sqrt(proxyErrorSq/N));

figure;
plot(t,result(:,3),t,result(:,4),t,result(:,7));
grid on;
xlabel('Time (s)'); ylabel('Output');
legend('Reference model','Full-state benchmark','Measured output');
title('Full-state and output-feedback information patterns');

function xn = rk4Plant(x,u,h,a0,a1,b)
    f = @(z) [z(2); -a0*z(1)-a1*z(2)+b*u];
    k1 = f(x); k2 = f(x+0.5*h*k1);
    k3 = f(x+0.5*h*k2); k4 = f(x+h*k3);
    xn = x + h*(k1+2*k2+2*k3+k4)/6;
end

function xn = rk4Model(x,r,h,wn,zeta)
    f = @(z) [z(2); -wn^2*z(1)-2*zeta*wn*z(2)+wn^2*r];
    k1 = f(x); k2 = f(x+0.5*h*k1);
    k3 = f(x+0.5*h*k2); k4 = f(x+h*k3);
    xn = x + h*(k1+2*k2+2*k3+k4)/6;
end

14. Wolfram Mathematica Implementation

Mathematica provides MatrixRank, symbolic state-space tools, and numerical differential-equation solvers. The notebook code below uses an explicit RK4 loop so that its numerical structure matches the other languages.

Chapter15_Lesson1.nb

(* Chapter15_Lesson1.nb *)
(* Paste into a Wolfram Mathematica notebook and evaluate. *)

ClearAll["Global`*"];
dt = 0.001; tFinal = 12.0; steps = Round[tFinal/dt] + 1;
a0 = 2.0; a1 = 0.8; b = 1.3;
wn = 1.4; zeta = 0.9;
kp = 3.0; kd = 2.0; kr = 3.0;
filterPole = 18.0; noiseAmplitude = 0.01;
SeedRandom[7];

amat = { {0.0, 1.0}, {-a0, -a1} };
cmat = { {1.0, 0.0} };
observability = Join[cmat, cmat.amat];
Print["Observability matrix = ", MatrixForm[observability]];
Print["Observability rank = ", MatrixRank[observability]];

reference[t_] := If[t >= 1.0, 1.0, 0.0];
plantRhs[x_, u_] := {x[[2]], -a0*x[[1]] - a1*x[[2]] + b*u};
modelRhs[x_, r_] := {x[[2]], -wn^2*x[[1]] - 2.0*zeta*wn*x[[2]] + wn^2*r};

rk4Step[rhs_, x_, h_, input_] := Module[{k1, k2, k3, k4},
  k1 = rhs[x, input];
  k2 = rhs[x + 0.5*h*k1, input];
  k3 = rhs[x + 0.5*h*k2, input];
  k4 = rhs[x + h*k3, input];
  x + h*(k1 + 2.0*k2 + 2.0*k3 + k4)/6.0
];

xFull = {0.0, 0.0}; xOutput = {0.0, 0.0}; xm = {0.0, 0.0};
filteredY = 0.0; errorSqFull = 0.0; errorSqOutput = 0.0;
proxyErrorSq = 0.0; rows = {};

Do[
  t = (k - 1)*dt; r = reference[t]; ym = xm[[1]];
  uFull = -kp*xFull[[1]] - kd*xFull[[2]] + kr*r;
  yMeasured = xOutput[[1]] + noiseAmplitude*RandomVariate[NormalDistribution[]];
  filteredY += dt*(-filterPole*filteredY + filterPole*yMeasured);
  x2Proxy = filterPole*(yMeasured - filteredY);
  uOutput = -kp*yMeasured - kd*x2Proxy + kr*r;

  AppendTo[rows, {t, r, ym, xFull[[1]], xFull[[2]], uFull,
                  yMeasured, xOutput[[2]], x2Proxy, uOutput}];
  errorSqFull += (xFull[[1]] - ym)^2;
  errorSqOutput += (xOutput[[1]] - ym)^2;
  proxyErrorSq += (x2Proxy - xOutput[[2]])^2;

  xFull = rk4Step[plantRhs, xFull, dt, uFull];
  xOutput = rk4Step[plantRhs, xOutput, dt, uOutput];
  xm = rk4Step[modelRhs, xm, dt, r];
, {k, 1, steps}];

header = {"time", "reference", "reference_output", "full_state_output",
          "full_state_velocity", "full_state_control", "measured_output",
          "hidden_velocity", "velocity_proxy", "output_feedback_control"};
Export["Chapter15_Lesson1_results.csv", Prepend[rows, header]];

Print["Full-state benchmark RMSE: ", Sqrt[errorSqFull/steps]];
Print["Output-feedback RMSE: ", Sqrt[errorSqOutput/steps]];
Print["Velocity-proxy RMSE: ", Sqrt[proxyErrorSq/steps]];

ListLinePlot[
  {rows[[All, {1, 3}]], rows[[All, {1, 4}]], rows[[All, {1, 7}]]},
  PlotLegends -> {"Reference model", "Full-state benchmark", "Measured output"},
  AxesLabel -> {"Time (s)", "Output"}, PlotRange -> All, ImageSize -> Large
]

15. Interpretation and Engineering Checks

The implementations deliberately retain the true velocity in the simulator output so it can be compared with the filtered proxy. That hidden state is prohibited from the output-feedback control calculation. This separation between simulation-only variables and controller-available variables is essential in credible adaptive-control experiments.

Before proposing an output-feedback adaptive law, check:

  1. Which signals are physically measured, and at what sampling rate?
  2. Which controller quantities are generated by stable causal dynamics?
  3. Is the uncertain plant uniformly observable or detectable over the admissible parameter set?
  4. Are relative degree, control direction, and minimum-phase assumptions stated?
  5. Does the stability proof include estimation/filter errors and their cross terms?
  6. Are noise, saturation, initialization, and peaking excluded by assumption or handled explicitly?

16. Problems and Solutions

Problem 1 (Instantaneous reconstruction): Let \( \mathbf{C}=[1\;0] \). Show that no static map of \( y \) can reconstruct \( \mathbf{x}=[x_1\;x_2]^T \) for every state.

Solution: States \( [x_1\;x_2]^T \) and \( [x_1\;x_2+q]^T \), with \( q\neq0 \), have the same measured output \( y=x_1 \). Therefore output at one instant does not determine velocity. Dynamic reconstruction must use how output evolves over time and how the known input acts on the plant.

Problem 2 (Observability of the running example): For \( \mathbf{A}=\begin{bmatrix}0&1\\-a_0&-a_1\end{bmatrix} \) and \( \mathbf{C}=[1\;0] \), calculate the observability matrix and determine whether observability depends on \( a_0,a_1 \).

Solution:

\[ \mathcal{O}=\begin{bmatrix}\mathbf{C}\\\mathbf{C}\mathbf{A}\end{bmatrix} =\begin{bmatrix}1&0\\0&1\end{bmatrix}. \]

Its determinant is one, so the rank is two for every finite value of \( a_0,a_1 \). The state is observable from position history and input even though velocity is not directly measured.

Problem 3 (Relative degree): For the running example with \( \mathbf{B}=[0\;b]^T \), find the relative degree from \( u \) to \( y \) when \( b\neq0 \).

Solution:

\[ \mathbf{C}\mathbf{B}=0,\qquad \mathbf{C}\mathbf{A}\mathbf{B}=b\neq0. \]

Hence the relative degree is two. Equivalently, \( \dot y=x_2 \) contains no input, while \( \ddot y=-a_0x_1-a_1x_2+bu \) contains the input.

Problem 4 (Filter error dynamics): For \( \dot z=-\lambda z+\lambda y \) and \( \hat{x}_{2,f}=\lambda(y-z) \), derive a differential equation for the proxy error \( \tilde{x}_{2,f}=x_2-\hat{x}_{2,f} \) in the noise-free running example.

Solution: Since \( \dot y=x_2 \),

\[ \dot{\hat{x} }_{2,f}=\lambda(\dot y-\dot z) =\lambda(x_2-\hat{x}_{2,f})=\lambda\tilde{x}_{2,f}. \]

Therefore

\[ \dot{\tilde{x} }_{2,f}=\dot x_2-\lambda\tilde{x}_{2,f} =-a_0x_1-a_1x_2+bu-\lambda\tilde{x}_{2,f}. \]

The proxy error is driven by plant acceleration; it is not autonomous and does not vanish merely because \( \lambda > 0 \). Increasing \( \lambda \) reduces low-frequency lag but increases noise sensitivity.

Problem 5 (Coupling in certainty-equivalent feedback): Suppose the ideal state-feedback law is \( u^{*}=\mathbf{k}^{*T}\mathbf{x}+k_r^{*}r \), while the implemented law is \( u=\hat{\mathbf{k} }^{T}\hat{\mathbf{x} }+\hat{k}_rr \). Express \( u-u^{*} \) in terms of controller-parameter error and state-estimation error.

Solution: Define \( \tilde{\mathbf{k} }=\hat{\mathbf{k} }-\mathbf{k}^{*} \), \( \tilde{k}_r=\hat{k}_r-k_r^{*} \), and \( \tilde{\mathbf{x} }=\mathbf{x}-\hat{\mathbf{x} } \). Since \( \hat{\mathbf{x} }=\mathbf{x}-\tilde{\mathbf{x} } \),

\[ u-u^{*}=\tilde{\mathbf{k} }^{T}\mathbf{x} -\hat{\mathbf{k} }^{T}\tilde{\mathbf{x} }+\tilde{k}_r r. \]

The middle term couples state-estimation error to the tracking loop, and its coefficient is itself time varying. A state-feedback Lyapunov proof that cancels only the first and third terms is incomplete for output feedback.

Problem 6 (Observable but nonminimum phase): Explain why observability alone does not guarantee that an output-tracking adaptive controller can be designed by stable inversion.

Solution: Observability guarantees uniqueness of state reconstruction from input-output history for a known model. Stable inversion additionally requires stable internal zero dynamics. An observable plant may contain a right-half-plane zero; forcing its output to follow an arbitrary reference through exact inversion then demands an unstable internal response or a noncausal input. Thus reconstruction and trackability are distinct structural questions.

17. Summary

Output-feedback adaptive control begins with an information constraint: the controller must be constructed from measured outputs, reference signals, past inputs, and internal dynamic states. Observability indicates whether missing state information can in principle be recovered, while relative degree and zero dynamics govern the input-output structure that tracking must respect. Stable filters can create measurable regressors, but filtering and estimation errors couple to time-varying parameter estimates, so the fixed-parameter separation principle cannot be assumed. The next lesson develops simple Luenberger-type structures that make these ideas constructive.

18. References

  1. Luenberger, D.G. (1964). Observing the state of a linear system. IEEE Transactions on Military Electronics, 8(2), 74–80.
  2. Kudva, P., and Narendra, K.S. (1973). Synthesis of an adaptive observer using Lyapunov's direct method. International Journal of Control, 18(6), 1201–1210.
  3. Lüders, G., and Narendra, K.S. (1973). An adaptive observer and identifier for a linear system. IEEE Transactions on Automatic Control, 18(5), 496–499.
  4. Lüders, G., and Narendra, K.S. (1974). A new canonical form for an adaptive observer. IEEE Transactions on Automatic Control, 19(2), 117–119.
  5. Kreisselmeier, G. (1977). Adaptive observers with exponential rate of convergence. IEEE Transactions on Automatic Control, 22(1), 2–8.
  6. Morse, A.S. (1980). Global stability of parameter-adaptive control systems. IEEE Transactions on Automatic Control, 25(3), 433–439.
  7. Boyd, S., and Sastry, S.S. (1986). Necessary and sufficient conditions for parameter convergence in adaptive control. Automatica, 22(6), 629–639.
  8. Narendra, K.S., and Annaswamy, A.M. (1987). Persistent excitation in adaptive systems. International Journal of Control, 45(1), 127–160.
  9. Bastin, G., and Gevers, M. (1988). Stable adaptive observers for nonlinear time-varying systems. IEEE Transactions on Automatic Control, 33(7), 650–658.
  10. Marino, R., and Tomei, P. (1997). Adaptive control of nonlinear systems: Basic results and open problems. Annual Reviews in Control, 21, 47–56.
Support CaaT Academy

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.