Chapter 15: Output-Feedback Adaptive Control – Basic Ideas

Lesson 2: Simple Observer Structures for Adaptive Use (Luenberger-Type)

This lesson develops the full-order Luenberger observer as the simplest state-reconstruction module that can be inserted between measured outputs and an adaptive controller. We derive observer-error dynamics, establish exponential convergence using both pole placement and Lyapunov analysis, quantify the effect of parameter mismatch and measurement noise, and implement a common second-order benchmark in Python, C++, Java, MATLAB/Simulink, and Wolfram Mathematica.

1. Learning Objectives and Adaptive-Control Context

In Chapter 15, Lesson 1, the output-feedback problem was formulated: the controller receives the measured output \( y(t) \), but a state-feedback control law or adaptation law may require the unmeasured state \( x(t) \). A state observer constructs an estimate \( \hat{x}(t) \) from the known input, the measured output, and a model of the plant.

After completing this lesson, the student should be able to:

  • derive the Luenberger observer-error equation and identify the role of the innovation term;
  • test observability and select the observer gain by dual pole placement;
  • prove exponential convergence with a quadratic Lyapunov function;
  • derive disturbance bounds caused by parameter mismatch and sensor noise;
  • explain why an observer designed for a fixed model does not automatically satisfy a separation principle inside an adaptive loop;
  • implement and validate the observer using several programming environments.
flowchart TD
  R["Reference r"] --> AC["Adaptive controller and update law"]
  XH["Estimated state x_hat"] --> AC
  AC --> U["Known plant input u"]
  U --> P["Plant with uncertain parameters"]
  P --> Y["Measured output y"]
  U --> O["Luenberger-type observer"]
  Y --> O
  TH["Current model or parameter \nestimate theta_hat"] --> O
  O --> XH
        

The observer is therefore an interface between output measurements and state-dependent controller calculations. It is not, by itself, a complete adaptive output-feedback controller.

2. Full-Order Luenberger Observer

Begin with the continuous-time linear time-invariant plant

\[ \dot{x}(t)=Ax(t)+Bu(t), \qquad y(t)=Cx(t), \]

where \( x\in\mathbb{R}^{n} \), \( u\in\mathbb{R}^{m} \), and \( y\in\mathbb{R}^{p} \). The full-order observer is

\[ \dot{\hat{x} }(t) =A\hat{x}(t)+Bu(t)+L\bigl(y(t)-C\hat{x}(t)\bigr). \]

The signal \( \nu(t)=y(t)-\hat{y}(t)=y(t)-C\hat{x}(t) \) is the innovation or output-estimation error. The correction matrix \( L\in\mathbb{R}^{n\times p} \) injects this measurable discrepancy into the observer dynamics.

Define the observer error \( e_o=x-\hat{x} \). Subtracting the observer from the plant gives

\[ \begin{aligned} \dot{e}_o &=\dot{x}-\dot{\hat{x} } \\ &=Ax+Bu-A\hat{x}-Bu-L(Cx-C\hat{x}) \\ &=(A-LC)(x-\hat{x}) \\ &=(A-LC)e_o. \end{aligned} \]

Thus the known input cancels exactly. The observer design problem is reduced to selecting \( L \) such that \( A-LC \) is Hurwitz:

\[ \operatorname{Re}\lambda_i(A-LC)<0, \qquad i=1,\ldots,n. \]

3. Observability and Observer-Gain Selection

Arbitrary observer-pole assignment is possible when the pair \( (A,C) \) is observable. The observability matrix is

\[ \mathcal{O} = \begin{bmatrix} C \\ CA \\ CA^2 \\ \vdots \\ CA^{n-1} \end{bmatrix}, \qquad \operatorname{rank}(\mathcal{O})=n. \]

Observer design is dual to state-feedback pole placement. Since \( (A,C) \) is observable exactly when \( (A^{T},C^{T}) \) is controllable, one may compute

\[ L = \operatorname{place}\!\left(A^{T},C^{T}, \{\lambda_{o,1},\ldots,\lambda_{o,n}\}\right)^{T}. \]

3.1 Explicit second-order formula

Consider the observable canonical structure

\[ A= \begin{bmatrix} 0&1\\ -a_0&-a_1 \end{bmatrix}, \qquad C=\begin{bmatrix}1&0\end{bmatrix}, \qquad L=\begin{bmatrix}\ell_1\\\ell_2\end{bmatrix}. \]

The observer-error matrix is

\[ A-LC= \begin{bmatrix} -\ell_1&1\\ -a_0-\ell_2&-a_1 \end{bmatrix}. \]

Its characteristic polynomial is

\[ \begin{aligned} \det\!\bigl(sI-(A-LC)\bigr) &=(s+\ell_1)(s+a_1)+(a_0+\ell_2)\\ &=s^2+(a_1+\ell_1)s+(a_0+a_1\ell_1+\ell_2). \end{aligned} \]

If the desired poles are \( -\omega_1 \) and \( -\omega_2 \), with \( \omega_1,\omega_2 \) positive, matching coefficients with \( (s+\omega_1)(s+\omega_2) \) yields

\[ \boxed{ \ell_1=\omega_1+\omega_2-a_1, \qquad \ell_2=\omega_1\omega_2-a_0-a_1\ell_1. } \]

4. Lyapunov Proof of Exponential Convergence

Let \( F=A-LC \) be Hurwitz. For any selected symmetric matrix \( Q=Q^{T}\succ0 \), there exists a unique symmetric matrix \( P=P^{T}\succ0 \) satisfying the Lyapunov equation

\[ F^{T}P+PF=-Q. \]

Choose the quadratic function

\[ V_o(e_o)=e_o^{T}Pe_o. \]

Using \( \dot{e}_o=Fe_o \),

\[ \begin{aligned} \dot{V}_o &=\dot{e}_o^{T}Pe_o+e_o^{T}P\dot{e}_o\\ &=e_o^{T}(F^{T}P+PF)e_o\\ &=-e_o^{T}Qe_o\\ &\leq-\lambda_{\min}(Q)\lVert e_o\rVert^2. \end{aligned} \]

Since

\[ \lambda_{\min}(P)\lVert e_o\rVert^2 \leq V_o \leq \lambda_{\max}(P)\lVert e_o\rVert^2, \]

we obtain

\[ \dot{V}_o \leq -\frac{\lambda_{\min}(Q)}{\lambda_{\max}(P)}V_o. \]

Therefore

\[ V_o(t) \leq V_o(0) \exp\!\left( -\frac{\lambda_{\min}(Q)}{\lambda_{\max}(P)}t \right), \]

and the state-estimation error satisfies the explicit bound

\[ \boxed{ \lVert e_o(t)\rVert \leq \sqrt{\frac{\lambda_{\max}(P)}{\lambda_{\min}(P)} } \exp\!\left( -\frac{\lambda_{\min}(Q)} {2\lambda_{\max}(P)}t \right) \lVert e_o(0)\rVert. } \]

This proves global exponential convergence for the exact-model, noise-free observer.

5. Observer Error with Parameter Mismatch

Adaptive control is motivated by uncertain plant parameters, so the observer generally cannot use the exact matrices. Let

\[ \dot{x}=A(\theta)x+B(\theta)u, \qquad y=Cx, \]

and use the certainty-equivalent observer

\[ \dot{\hat{x} } = A(\hat{\theta})\hat{x} +B(\hat{\theta})u +L\bigl(y-C\hat{x}\bigr). \]

With \( e_o=x-\hat{x} \), direct subtraction gives

\[ \boxed{ \dot{e}_o = \bigl(A(\hat{\theta})-LC\bigr)e_o + \bigl(A(\theta)-A(\hat{\theta})\bigr)x + \bigl(B(\theta)-B(\hat{\theta})\bigr)u. } \]

Define the mismatch forcing

\[ d_{\theta} = \bigl(A(\theta)-A(\hat{\theta})\bigr)x + \bigl(B(\theta)-B(\hat{\theta})\bigr)u. \]

If the matrix mappings are locally Lipschitz in the parameter vector,

\[ \begin{aligned} \lVert A(\theta)-A(\hat{\theta})\rVert &\leq \ell_A\lVert\tilde{\theta}\rVert,\\ \lVert B(\theta)-B(\hat{\theta})\rVert &\leq \ell_B\lVert\tilde{\theta}\rVert, \qquad \tilde{\theta}=\theta-\hat{\theta}, \end{aligned} \]

then

\[ \lVert d_{\theta}\rVert \leq \bigl(\ell_A\lVert x\rVert+\ell_B\lVert u\rVert\bigr) \lVert\tilde{\theta}\rVert. \]

5.1 Input-to-state bound for a fixed stable observer matrix

Suppose \( F=A(\hat{\theta})-LC \) is constant and Hurwitz, and constants \( M\geq1 \), \( \alpha>0 \) satisfy

\[ \lVert e^{Ft}\rVert\leq Me^{-\alpha t}. \]

Variation of constants gives

\[ e_o(t) = e^{Ft}e_o(0) + \int_{0}^{t}e^{F(t-\tau)}d_{\theta}(\tau)\,d\tau. \]

Hence

\[ \boxed{ \lVert e_o(t)\rVert \leq Me^{-\alpha t}\lVert e_o(0)\rVert + \frac{M}{\alpha} \sup_{0\leq\tau\leq t}\lVert d_{\theta}(\tau)\rVert. } \]

A fixed model error therefore generally produces a bounded residual state-estimation error. If the parameter estimate converges, the plant signals remain bounded, and \( d_{\theta}(t)→0 \), then asymptotic observer convergence may be recovered under suitable uniform-stability conditions.

A critical warning is that \( A(\hat{\theta}(t))-LC \) is time varying. Stability of every frozen matrix does not alone prove uniform exponential stability of the time-varying error system. This coupling is one reason the classical fixed-parameter separation principle cannot be transferred mechanically to adaptive systems; Lesson 3 studies this issue directly.

6. How Observer Error Enters an Adaptive Controller

Suppose a state-dependent controller would ideally use

\[ u_c=\hat{K}^{T}\psi(x,r), \]

but only the estimated state is available:

\[ \hat{u}_c=\hat{K}^{T}\psi(\hat{x},r). \]

If the regressor map is Lipschitz in its state argument,

\[ \lVert\psi(\hat{x},r)-\psi(x,r)\rVert \leq \ell_{\psi}\lVert e_o\rVert, \]

then the control perturbation is bounded by

\[ \begin{aligned} \delta u_c &= \hat{K}^{T} \bigl(\psi(\hat{x},r)-\psi(x,r)\bigr),\\ \lVert\delta u_c\rVert &\leq \lVert\hat{K}\rVert\ell_{\psi}\lVert e_o\rVert. \end{aligned} \]

Similarly, replacing a full-state adaptation regressor \( \phi(x,u,r) \) with \( \phi(\hat{x},u,r) \) introduces an additional perturbation proportional to the observer error. Consequently:

  • fast observer convergence reduces the duration of the regressor perturbation;
  • excessive observer gain can amplify sensor noise and inject it into both the controller and the parameter update;
  • bounded parameter estimates, projection, normalization, and robust modifications from earlier chapters remain relevant;
  • a proof for the combined adaptive-controller/observer dynamics is still required.

7. Measurement Noise, High Gain, and Peaking

Let the measured output be \( y_m=Cx+n \), where \( n(t) \) is sensor noise. The observer uses

\[ \dot{\hat{x} } = A\hat{x}+Bu+L(y_m-C\hat{x}). \]

The error equation becomes

\[ \boxed{ \dot{e}_o=(A-LC)e_o-Ln. } \]

Thus faster poles usually require larger entries in \( L \), which increases the direct noise-injection term \( -Ln \). In the frequency domain,

\[ E_o(s) = -(sI-A+LC)^{-1}L\,N(s). \]

Observer bandwidth must therefore be selected as a compromise among convergence speed, model uncertainty, sampling rate, and sensor-noise bandwidth.

7.1 Peaking

A large initial error combined with a high-gain observer can produce a large transient estimate before convergence. If \( \hat{x} \) is immediately used in an adaptive controller, this transient may cause:

  • temporary actuator saturation;
  • large parameter-update transients;
  • activation of projection boundaries;
  • numerical stiffness in continuous-time simulation.

Practical mitigations include moderate pole placement, bounded initial estimates, saturation-aware control, temporary adaptation-rate limiting, and validation with sensor-noise and initial-condition sweeps.

8. Sampled and Numerical Implementation

Chapter 14 introduced discrete-time adaptive structures. For an exact zero-order-hold discretization,

\[ A_d=e^{AT_s}, \qquad B_d=\int_{0}^{T_s}e^{A\tau}B\,d\tau, \]

a discrete observer can be written as

\[ \hat{x}_{k+1} = A_d\hat{x}_k+B_du_k+L_d(y_k-C\hat{x}_k), \]

with error dynamics

\[ e_{o,k+1}=(A_d-L_dC)e_{o,k}. \]

All eigenvalues of \( A_d-L_dC \) must lie strictly inside the unit circle. When a continuous observer is numerically integrated instead, the integration step must also resolve the fastest observer pole. A common engineering rule is to use several tens of numerical steps over the smallest observer time constant and then verify convergence by halving the step size.

9. Worked Second-Order Benchmark

The implementations in this lesson use the stable plant

\[ A= \begin{bmatrix} 0&1\\ -2&-0.7 \end{bmatrix}, \qquad B= \begin{bmatrix} 0\\1 \end{bmatrix}, \qquad C= \begin{bmatrix} 1&0 \end{bmatrix}. \]

Since

\[ \mathcal{O} = \begin{bmatrix} 1&0\\ 0&1 \end{bmatrix}, \]

the pair is observable. Choose observer poles at \( -4 \) and \( -5 \). Using the formula from Section 3,

\[ \ell_1=4+5-0.7=8.3, \qquad \ell_2=20-2-0.7(8.3)=12.19. \]

Therefore

\[ L= \begin{bmatrix} 8.3\\12.19 \end{bmatrix}, \qquad A-LC= \begin{bmatrix} -8.3&1\\ -14.19&-0.7 \end{bmatrix}, \]

and

\[ \det\!\bigl(sI-(A-LC)\bigr) =s^2+9s+20=(s+4)(s+5). \]

For \( Q=I \), the Lyapunov equation produces approximately

\[ P= \begin{bmatrix} 0.61624&-0.32521\\ -0.32521&0.24969 \end{bmatrix}, \qquad \lambda(P)\approx\{0.05967,\;0.80627\}. \]

The positive eigenvalues verify \( P\succ0 \). The code also uses the nominal model

\[ \hat{A}_{0} = \begin{bmatrix} 0&1\\ -1.6&-0.5 \end{bmatrix}, \]

to demonstrate that stable nominal observer poles do not eliminate the forcing caused by plant-model mismatch.

10. Python Implementation

The Python implementation uses NumPy, SciPy, and Matplotlib. It compares a matched observer, a fixed mismatched observer, and a model whose parameters gradually approach the true values. The scheduled model is only a placeholder for estimates supplied by an external RLS or gradient estimator already introduced in earlier chapters.

Chapter15_Lesson2.py

"""
Chapter15_Lesson2.py

Luenberger-type observer benchmark for adaptive-control use.

The script compares:
1. a matched observer,
2. a fixed nominal-model observer, and
3. an observer whose model parameters are supplied by an external,
   gradually improving estimate.

The scheduled estimate is only a stand-in for an estimator developed in earlier
chapters; this lesson does not introduce a new parameter-adaptation law.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Literal

import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import solve_ivp
from scipy.signal import place_poles


Mode = Literal["matched", "fixed-mismatch", "scheduled"]


@dataclass(frozen=True)
class SecondOrderModel:
    a0: float
    a1: float

    @property
    def A(self) -> np.ndarray:
        return np.array([[0.0, 1.0], [-self.a0, -self.a1]], dtype=float)


B = np.array([[0.0], [1.0]], dtype=float)
C = np.array([[1.0, 0.0]], dtype=float)

TRUE_MODEL = SecondOrderModel(a0=2.0, a1=0.7)
NOMINAL_MODEL = SecondOrderModel(a0=1.6, a1=0.5)
DESIRED_OBSERVER_POLES = np.array([-4.0, -5.0])

L_TRUE = place_poles(
    TRUE_MODEL.A.T, C.T, DESIRED_OBSERVER_POLES
).gain_matrix.T
L_NOMINAL = place_poles(
    NOMINAL_MODEL.A.T, C.T, DESIRED_OBSERVER_POLES
).gain_matrix.T


def input_signal(t: float) -> float:
    """Persistently varying known plant input used by both plant and observer."""
    return 0.8 * np.sin(0.7 * t) + 0.4 * np.sin(1.9 * t)


def scheduled_model(t: float) -> SecondOrderModel:
    """
    Emulate parameters supplied by an external estimator.

    The estimate starts at the nominal model and approaches the true model.
    No claim is made that this schedule is itself an adaptive law.
    """
    rho = np.exp(-0.35 * t)
    return SecondOrderModel(
        a0=TRUE_MODEL.a0 + (NOMINAL_MODEL.a0 - TRUE_MODEL.a0) * rho,
        a1=TRUE_MODEL.a1 + (NOMINAL_MODEL.a1 - TRUE_MODEL.a1) * rho,
    )


def observer_data(t: float, mode: Mode) -> tuple[np.ndarray, np.ndarray]:
    if mode == "matched":
        return TRUE_MODEL.A, L_TRUE
    if mode == "fixed-mismatch":
        return NOMINAL_MODEL.A, L_NOMINAL
    if mode == "scheduled":
        return scheduled_model(t).A, L_NOMINAL
    raise ValueError(f"Unsupported mode: {mode}")


def augmented_rhs(t: float, z: np.ndarray, mode: Mode) -> np.ndarray:
    x = z[0:2]
    x_hat = z[2:4]

    u = input_signal(t)
    y = (C @ x).item()

    x_dot = TRUE_MODEL.A @ x + B[:, 0] * u

    A_hat, L = observer_data(t, mode)
    innovation = y - (C @ x_hat).item()
    x_hat_dot = A_hat @ x_hat + B[:, 0] * u + L[:, 0] * innovation

    return np.concatenate((x_dot, x_hat_dot))


def simulate(mode: Mode) -> tuple[np.ndarray, np.ndarray]:
    t_eval = np.linspace(0.0, 20.0, 4001)
    z0 = np.array([1.0, -0.5, 0.0, 0.0], dtype=float)

    solution = solve_ivp(
        fun=lambda t, z: augmented_rhs(t, z, mode),
        t_span=(t_eval[0], t_eval[-1]),
        y0=z0,
        t_eval=t_eval,
        method="RK45",
        rtol=1.0e-9,
        atol=1.0e-11,
    )
    if not solution.success:
        raise RuntimeError(solution.message)
    return solution.t, solution.y


def main() -> None:
    print("Desired observer poles:", DESIRED_OBSERVER_POLES)
    print("Matched gain L =", L_TRUE.ravel())
    print("Nominal gain L =", L_NOMINAL.ravel())
    print(
        "Matched error eigenvalues:",
        np.linalg.eigvals(TRUE_MODEL.A - L_TRUE @ C),
    )
    print(
        "Nominal error eigenvalues:",
        np.linalg.eigvals(NOMINAL_MODEL.A - L_NOMINAL @ C),
    )

    modes: tuple[Mode, ...] = ("matched", "fixed-mismatch", "scheduled")
    results: dict[Mode, tuple[np.ndarray, np.ndarray]] = {}

    for mode in modes:
        t, z = simulate(mode)
        results[mode] = (t, z)
        error = z[0:2, :] - z[2:4, :]
        final_norm = np.linalg.norm(error[:, -1])
        rms_norm = np.sqrt(np.mean(np.sum(error * error, axis=0)))
        print(f"{mode:>14s}: final ||e_o|| = {final_norm:.6e}, RMS = {rms_norm:.6e}")

    t, z_matched = results["matched"]
    _, z_mismatch = results["fixed-mismatch"]
    _, z_scheduled = results["scheduled"]

    plt.figure(figsize=(10, 6))
    plt.plot(t, z_matched[0], label="x1")
    plt.plot(t, z_matched[2], "--", label="x1_hat, matched")
    plt.plot(t, z_matched[1], label="x2")
    plt.plot(t, z_matched[3], "--", label="x2_hat, matched")
    plt.xlabel("Time (s)")
    plt.ylabel("State")
    plt.title("Matched Luenberger observer")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()
    plt.savefig("Chapter15_Lesson2_python_states.png", dpi=180)

    plt.figure(figsize=(10, 6))
    for mode, z in (
        ("matched", z_matched),
        ("fixed mismatch", z_mismatch),
        ("scheduled estimate", z_scheduled),
    ):
        error_norm = np.linalg.norm(z[0:2, :] - z[2:4, :], axis=0)
        plt.semilogy(t, np.maximum(error_norm, 1.0e-12), label=mode)
    plt.xlabel("Time (s)")
    plt.ylabel("Observer-error norm")
    plt.title("Effect of model mismatch on state reconstruction")
    plt.grid(True)
    plt.legend()
    plt.tight_layout()
    plt.savefig("Chapter15_Lesson2_python_error.png", dpi=180)

    if "agg" not in plt.get_backend().lower():
        plt.show()


if __name__ == "__main__":
    main()

11. C++ Implementation

The C++ program uses only the standard library. It implements the observer and fourth-order Runge–Kutta integration from scratch and writes matched and mismatched trajectories to CSV files.

Chapter15_Lesson2.cpp

/*
Chapter15_Lesson2.cpp

From-scratch RK4 simulation of a second-order plant and a full-order
Luenberger observer. The program compares matched and mismatched observer
models and writes the trajectories to CSV files.
*/

#include <array>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <string>

struct Model {
    double a0;
    double a1;
};

struct ObserverGain {
    double l1;
    double l2;
};

using State = std::array<double, 4>;

double inputSignal(double t) {
    return 0.8 * std::sin(0.7 * t) + 0.4 * std::sin(1.9 * t);
}

ObserverGain placeSecondOrderObserver(
    const Model& model,
    double pole1,
    double pole2
) {
    // Desired polynomial:
    // (s - pole1)(s - pole2) = s^2 + alpha1 s + alpha0.
    const double alpha1 = -(pole1 + pole2);
    const double alpha0 = pole1 * pole2;

    // For A = [[0,1],[-a0,-a1]], C = [1,0], L = [l1,l2]^T:
    // det(sI - (A-LC)) = s^2 + (a1+l1)s + a0+a1*l1+l2.
    ObserverGain gain{};
    gain.l1 = alpha1 - model.a1;
    gain.l2 = alpha0 - model.a0 - model.a1 * gain.l1;
    return gain;
}

State derivative(
    double t,
    const State& z,
    const Model& trueModel,
    const Model& observerModel,
    const ObserverGain& gain
) {
    const double x1 = z[0];
    const double x2 = z[1];
    const double xHat1 = z[2];
    const double xHat2 = z[3];

    const double u = inputSignal(t);
    const double y = x1;
    const double innovation = y - xHat1;

    State dz{};
    dz[0] = x2;
    dz[1] = -trueModel.a0 * x1 - trueModel.a1 * x2 + u;
    dz[2] = xHat2 + gain.l1 * innovation;
    dz[3] =
        -observerModel.a0 * xHat1
        -observerModel.a1 * xHat2
        + u
        + gain.l2 * innovation;
    return dz;
}

State addScaled(const State& a, const State& b, double scale) {
    State result{};
    for (std::size_t i = 0; i < result.size(); ++i) {
        result[i] = a[i] + scale * b[i];
    }
    return result;
}

State rk4Step(
    double t,
    const State& z,
    double h,
    const Model& trueModel,
    const Model& observerModel,
    const ObserverGain& gain
) {
    const State k1 = derivative(t, z, trueModel, observerModel, gain);
    const State k2 = derivative(
        t + 0.5 * h,
        addScaled(z, k1, 0.5 * h),
        trueModel,
        observerModel,
        gain
    );
    const State k3 = derivative(
        t + 0.5 * h,
        addScaled(z, k2, 0.5 * h),
        trueModel,
        observerModel,
        gain
    );
    const State k4 = derivative(
        t + h,
        addScaled(z, k3, h),
        trueModel,
        observerModel,
        gain
    );

    State next{};
    for (std::size_t i = 0; i < next.size(); ++i) {
        next[i] = z[i] + (h / 6.0) * (
            k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]
        );
    }
    return next;
}

double observerErrorNorm(const State& z) {
    const double e1 = z[0] - z[2];
    const double e2 = z[1] - z[3];
    return std::sqrt(e1 * e1 + e2 * e2);
}

void runScenario(
    const std::string& name,
    const Model& trueModel,
    const Model& observerModel
) {
    const ObserverGain gain = placeSecondOrderObserver(
        observerModel,
        -4.0,
        -5.0
    );

    const std::string fileName =
        "Chapter15_Lesson2_cpp_" + name + ".csv";
    std::ofstream csv(fileName);
    if (!csv) {
        throw std::runtime_error("Cannot open output file: " + fileName);
    }

    csv << "time,x1,x2,xhat1,xhat2,error_norm\n";
    csv << std::setprecision(12);

    const double h = 0.001;
    const double finalTime = 20.0;
    State z{1.0, -0.5, 0.0, 0.0};

    double squaredErrorIntegral = 0.0;
    int samples = 0;

    for (double t = 0.0; t <= finalTime + 0.5 * h; t += h) {
        const double errorNorm = observerErrorNorm(z);
        csv << t << ','
            << z[0] << ','
            << z[1] << ','
            << z[2] << ','
            << z[3] << ','
            << errorNorm << '\n';

        squaredErrorIntegral += errorNorm * errorNorm;
        ++samples;

        z = rk4Step(
            t,
            z,
            h,
            trueModel,
            observerModel,
            gain
        );
    }

    const double rmsError =
        std::sqrt(squaredErrorIntegral / static_cast<double>(samples));

    std::cout << name
              << ": L = [" << gain.l1 << ", " << gain.l2 << "]^T"
              << ", final ||e_o|| = " << observerErrorNorm(z)
              << ", RMS = " << rmsError
              << '\n';
}

int main() {
    try {
        const Model trueModel{2.0, 0.7};
        const Model nominalModel{1.6, 0.5};

        runScenario("matched", trueModel, trueModel);
        runScenario("fixed_mismatch", trueModel, nominalModel);
    } catch (const std::exception& exception) {
        std::cerr << "Error: " << exception.what() << '\n';
        return 1;
    }
    return 0;
}

12. Java Implementation

The Java implementation mirrors the C++ benchmark and uses no external numerical library. It is suitable for illustrating how observer states can be integrated inside a real-time Java control application.

Chapter15_Lesson2.java

/*
Chapter15_Lesson2.java

From-scratch RK4 simulation of a second-order plant and a full-order
Luenberger observer. The program compares matched and mismatched observer
models and writes trajectories to CSV files.
*/

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

public final class Chapter15_Lesson2 {

    private record Model(double a0, double a1) {}

    private record ObserverGain(double l1, double l2) {}

    private Chapter15_Lesson2() {
        // Utility class.
    }

    private static double inputSignal(double t) {
        return 0.8 * Math.sin(0.7 * t) + 0.4 * Math.sin(1.9 * t);
    }

    private static ObserverGain placeSecondOrderObserver(
        Model model,
        double pole1,
        double pole2
    ) {
        // (s - pole1)(s - pole2) = s^2 + alpha1*s + alpha0.
        double alpha1 = -(pole1 + pole2);
        double alpha0 = pole1 * pole2;

        double l1 = alpha1 - model.a1();
        double l2 = alpha0 - model.a0() - model.a1() * l1;
        return new ObserverGain(l1, l2);
    }

    private static double[] derivative(
        double t,
        double[] z,
        Model trueModel,
        Model observerModel,
        ObserverGain gain
    ) {
        double x1 = z[0];
        double x2 = z[1];
        double xHat1 = z[2];
        double xHat2 = z[3];

        double u = inputSignal(t);
        double y = x1;
        double innovation = y - xHat1;

        return new double[] {
            x2,
            -trueModel.a0() * x1 - trueModel.a1() * x2 + u,
            xHat2 + gain.l1() * innovation,
            -observerModel.a0() * xHat1
                - observerModel.a1() * xHat2
                + u
                + gain.l2() * innovation
        };
    }

    private static double[] addScaled(
        double[] a,
        double[] b,
        double scale
    ) {
        double[] result = new double[a.length];
        for (int i = 0; i < a.length; i++) {
            result[i] = a[i] + scale * b[i];
        }
        return result;
    }

    private static double[] rk4Step(
        double t,
        double[] z,
        double h,
        Model trueModel,
        Model observerModel,
        ObserverGain gain
    ) {
        double[] k1 = derivative(
            t, z, trueModel, observerModel, gain
        );
        double[] k2 = derivative(
            t + 0.5 * h,
            addScaled(z, k1, 0.5 * h),
            trueModel,
            observerModel,
            gain
        );
        double[] k3 = derivative(
            t + 0.5 * h,
            addScaled(z, k2, 0.5 * h),
            trueModel,
            observerModel,
            gain
        );
        double[] k4 = derivative(
            t + h,
            addScaled(z, k3, h),
            trueModel,
            observerModel,
            gain
        );

        double[] next = new double[z.length];
        for (int i = 0; i < z.length; i++) {
            next[i] = z[i] + (h / 6.0) * (
                k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]
            );
        }
        return next;
    }

    private static double observerErrorNorm(double[] z) {
        double e1 = z[0] - z[2];
        double e2 = z[1] - z[3];
        return Math.hypot(e1, e2);
    }

    private static void runScenario(
        String name,
        Model trueModel,
        Model observerModel
    ) throws IOException {
        ObserverGain gain = placeSecondOrderObserver(
            observerModel,
            -4.0,
            -5.0
        );

        String fileName =
            "Chapter15_Lesson2_java_" + name + ".csv";

        try (
            PrintWriter csv = new PrintWriter(
                new BufferedWriter(new FileWriter(fileName))
            )
        ) {
            csv.println("time,x1,x2,xhat1,xhat2,error_norm");

            double h = 0.001;
            double finalTime = 20.0;
            double[] z = {1.0, -0.5, 0.0, 0.0};

            double squaredErrorSum = 0.0;
            int samples = 0;

            for (
                double t = 0.0;
                t <= finalTime + 0.5 * h;
                t += h
            ) {
                double errorNorm = observerErrorNorm(z);
                csv.printf(
                    Locale.US,
                    "%.12f,%.12f,%.12f,%.12f,%.12f,%.12f%n",
                    t,
                    z[0],
                    z[1],
                    z[2],
                    z[3],
                    errorNorm
                );

                squaredErrorSum += errorNorm * errorNorm;
                samples++;

                z = rk4Step(
                    t,
                    z,
                    h,
                    trueModel,
                    observerModel,
                    gain
                );
            }

            double rmsError = Math.sqrt(
                squaredErrorSum / samples
            );

            System.out.printf(
                Locale.US,
                "%s: L = [%.6f, %.6f]^T, final ||e_o|| = %.6e, RMS = %.6e%n",
                name,
                gain.l1(),
                gain.l2(),
                observerErrorNorm(z),
                rmsError
            );
        }
    }

    public static void main(String[] args) {
        Model trueModel = new Model(2.0, 0.7);
        Model nominalModel = new Model(1.6, 0.5);

        try {
            runScenario("matched", trueModel, trueModel);
            runScenario(
                "fixed_mismatch",
                trueModel,
                nominalModel
            );
        } catch (IOException exception) {
            System.err.println(
                "I/O error: " + exception.getMessage()
            );
            System.exit(1);
        }
    }
}

13. MATLAB and Simulink Implementation

The MATLAB script uses ode45 and place when the Control System Toolbox is available. A closed-form fallback computes the second-order observer gain. The final function programmatically creates Chapter15_Lesson2_Simulink.slx using Plant, Observer, innovation-routing, and output-comparison blocks.

Chapter15_Lesson2.m

% Chapter15_Lesson2.m
%
% Luenberger-type observer benchmark for adaptive-control use.
% The script compares matched, fixed-mismatch, and externally scheduled
% observer models. It also creates a simple Simulink observer model when
% Simulink is available.

clear;
clc;
close all;

A_true = [0 1; -2.0 -0.7];
A_nominal = [0 1; -1.6 -0.5];
B = [0; 1];
C = [1 0];

observer_poles = [-4 -5];
L_true = observer_gain(A_true, C, observer_poles);
L_nominal = observer_gain(A_nominal, C, observer_poles);

fprintf('Matched L = [%g, %g]^T\n', L_true(1), L_true(2));
fprintf('Nominal L = [%g, %g]^T\n', L_nominal(1), L_nominal(2));
disp('Matched observer-error eigenvalues:');
disp(eig(A_true - L_true * C));
disp('Nominal observer-error eigenvalues:');
disp(eig(A_nominal - L_nominal * C));

t_span = linspace(0, 20, 4001);
z0 = [1; -0.5; 0; 0];

modes = ["matched", "fixed-mismatch", "scheduled"];
results = struct();

for k = 1:numel(modes)
    mode = modes(k);
    rhs = @(t, z) augmented_rhs( ...
        t, ...
        z, ...
        mode, ...
        A_true, ...
        A_nominal, ...
        B, ...
        C, ...
        L_true, ...
        L_nominal ...
    );

    options = odeset('RelTol', 1e-9, 'AbsTol', 1e-11);
    [t, z] = ode45(rhs, t_span, z0, options);

    field_name = matlab.lang.makeValidName(mode);
    results.(field_name).t = t;
    results.(field_name).z = z;

    error = z(:, 1:2) - z(:, 3:4);
    error_norm = vecnorm(error, 2, 2);
    fprintf( ...
        '%s: final ||e_o|| = %.6e, RMS = %.6e\n', ...
        mode, ...
        error_norm(end), ...
        sqrt(mean(error_norm .^ 2)) ...
    );
end

matched = results.matched;
fixed_mismatch = results.fixed_mismatch;
scheduled = results.scheduled;

figure('Name', 'Matched observer');
plot(matched.t, matched.z(:, 1), 'LineWidth', 1.2);
hold on;
plot(matched.t, matched.z(:, 3), '--', 'LineWidth', 1.2);
plot(matched.t, matched.z(:, 2), 'LineWidth', 1.2);
plot(matched.t, matched.z(:, 4), '--', 'LineWidth', 1.2);
grid on;
xlabel('Time (s)');
ylabel('State');
title('Matched Luenberger observer');
legend('x_1', 'xhat_1', 'x_2', 'xhat_2', 'Location', 'best');

figure('Name', 'Observer-error comparison');
semilogy( ...
    matched.t, ...
    max(vecnorm(matched.z(:, 1:2) - matched.z(:, 3:4), 2, 2), 1e-12), ...
    'LineWidth', ...
    1.2 ...
);
hold on;
semilogy( ...
    fixed_mismatch.t, ...
    max(vecnorm( ...
        fixed_mismatch.z(:, 1:2) - fixed_mismatch.z(:, 3:4), ...
        2, ...
        2 ...
    ), 1e-12), ...
    'LineWidth', ...
    1.2 ...
);
semilogy( ...
    scheduled.t, ...
    max(vecnorm( ...
        scheduled.z(:, 1:2) - scheduled.z(:, 3:4), ...
        2, ...
        2 ...
    ), 1e-12), ...
    'LineWidth', ...
    1.2 ...
);
grid on;
xlabel('Time (s)');
ylabel('Observer-error norm');
title('Effect of model mismatch');
legend('matched', 'fixed mismatch', 'scheduled estimate', 'Location', 'best');

build_simulink_model = true;
if build_simulink_model
    try
        build_observer_simulink_model( ...
            A_true, ...
            A_nominal, ...
            B, ...
            C, ...
            L_nominal ...
        );
        fprintf( ...
            'Created and opened Chapter15_Lesson2_Simulink.slx\n' ...
        );
    catch exception
        warning( ...
            'Simulink model was not created: %s', ...
            exception.message ...
        );
    end
end


function dz = augmented_rhs( ...
    t, ...
    z, ...
    mode, ...
    A_true, ...
    A_nominal, ...
    B, ...
    C, ...
    L_true, ...
    L_nominal ...
)
    x = z(1:2);
    x_hat = z(3:4);

    u = 0.8 * sin(0.7 * t) + 0.4 * sin(1.9 * t);
    y = C * x;

    if mode == "matched"
        A_hat = A_true;
        L = L_true;
    elseif mode == "fixed-mismatch"
        A_hat = A_nominal;
        L = L_nominal;
    elseif mode == "scheduled"
        rho = exp(-0.35 * t);
        A_hat = A_true + rho * (A_nominal - A_true);
        L = L_nominal;
    else
        error('Unknown simulation mode.');
    end

    x_dot = A_true * x + B * u;
    x_hat_dot = A_hat * x_hat + B * u + L * (y - C * x_hat);
    dz = [x_dot; x_hat_dot];
end


function L = observer_gain(A, C, poles)
    if exist('place', 'file') == 2
        L = place(A', C', poles)';
        return;
    end

    % Fallback for the second-order observable canonical form used here.
    a0 = -A(2, 1);
    a1 = -A(2, 2);
    alpha1 = -sum(poles);
    alpha0 = prod(poles);
    l1 = alpha1 - a1;
    l2 = alpha0 - a0 - a1 * l1;
    L = [l1; l2];
end


function build_observer_simulink_model( ...
    A_true, ...
    A_observer, ...
    B, ...
    C, ...
    L ...
)
    model_name = 'Chapter15_Lesson2_Simulink';

    if bdIsLoaded(model_name)
        close_system(model_name, 0);
    end

    new_system(model_name);
    open_system(model_name);

    add_block( ...
        'simulink/Sources/Sine Wave', ...
        [model_name '/Sine 1'], ...
        'Amplitude', ...
        '0.8', ...
        'Frequency', ...
        '0.7', ...
        'Position', ...
        [30 55 80 85] ...
    );
    add_block( ...
        'simulink/Sources/Sine Wave', ...
        [model_name '/Sine 2'], ...
        'Amplitude', ...
        '0.4', ...
        'Frequency', ...
        '1.9', ...
        'Position', ...
        [30 115 80 145] ...
    );
    add_block( ...
        'simulink/Math Operations/Sum', ...
        [model_name '/Input Sum'], ...
        'Inputs', ...
        '++', ...
        'Position', ...
        [120 75 145 130] ...
    );

    add_block( ...
        'simulink/Continuous/State-Space', ...
        [model_name '/Plant'], ...
        'A', ...
        mat2str(A_true), ...
        'B', ...
        mat2str(B), ...
        'C', ...
        mat2str(C), ...
        'D', ...
        '0', ...
        'X0', ...
        '[1; -0.5]', ...
        'Position', ...
        [210 60 340 115] ...
    );

    add_block( ...
        'simulink/Signal Routing/Mux', ...
        [model_name '/Observer Input'], ...
        'Inputs', ...
        '2', ...
        'Position', ...
        [400 70 405 145] ...
    );

    observer_A = A_observer - L * C;
    observer_B = [B L];

    add_block( ...
        'simulink/Continuous/State-Space', ...
        [model_name '/Observer'], ...
        'A', ...
        mat2str(observer_A), ...
        'B', ...
        mat2str(observer_B), ...
        'C', ...
        'eye(2)', ...
        'D', ...
        'zeros(2,2)', ...
        'X0', ...
        '[0; 0]', ...
        'Position', ...
        [460 75 600 135] ...
    );

    add_block( ...
        'simulink/Math Operations/Gain', ...
        [model_name '/Estimated Output'], ...
        'Gain', ...
        mat2str(C), ...
        'Multiplication', ...
        'Matrix(K*u)', ...
        'Position', ...
        [650 85 735 125] ...
    );

    add_block( ...
        'simulink/Signal Routing/Mux', ...
        [model_name '/Output Comparison'], ...
        'Inputs', ...
        '2', ...
        'Position', ...
        [790 65 795 145] ...
    );
    add_block( ...
        'simulink/Sinks/Scope', ...
        [model_name '/y and yhat'], ...
        'Position', ...
        [850 80 900 130] ...
    );

    add_line(model_name, 'Sine 1/1', 'Input Sum/1');
    add_line(model_name, 'Sine 2/1', 'Input Sum/2');
    add_line(model_name, 'Input Sum/1', 'Plant/1');
    add_line(model_name, 'Input Sum/1', 'Observer Input/1');
    add_line(model_name, 'Plant/1', 'Observer Input/2');
    add_line(model_name, 'Observer Input/1', 'Observer/1');
    add_line(model_name, 'Observer/1', 'Estimated Output/1');
    add_line(model_name, 'Plant/1', 'Output Comparison/1');
    add_line(model_name, 'Estimated Output/1', 'Output Comparison/2');
    add_line(model_name, 'Output Comparison/1', 'y and yhat/1');

    set_param(model_name, 'StopTime', '20');
    save_system(model_name, [model_name '.slx']);
end

14. Wolfram Mathematica Implementation

The notebook file loads the executable Wolfram Language source from the same directory. The source uses NDSolveValue, Eigenvalues, and logarithmic error plots.

Chapter15_Lesson2.nb

Notebook[{
  Cell["Chapter 15, Lesson 2: Simple Observer Structures for Adaptive Use", "Title"],
  Cell[
    "The executable Wolfram Language source is stored in Chapter15_Lesson2.wl in the same directory.",
    "Text"
  ],
  Cell[
    BoxData[
      RowBox[{
        "Get",
        "[",
        RowBox[{
          RowBox[{"NotebookDirectory", "[", "]"}],
          "<>",
          "\"Chapter15_Lesson2.wl\""
        }],
        "]"
      }]
    ],
    "Input"
  ]
},
WindowTitle -> "Chapter15_Lesson2",
Saveable -> True
]

Chapter15_Lesson2.wl

(* Chapter15_Lesson2.wl
   Wolfram Language implementation of a second-order Luenberger observer.
*)

ClearAll["Global`*"];

aTrue = { {0., 1.}, {-2.0, -0.7} };
aNominal = { {0., 1.}, {-1.6, -0.5} };
b = {0., 1.};
c = { {1., 0.} };
observerPoles = {-4., -5.};

observerGain[a_?MatrixQ, cRow_?MatrixQ, poles_List] := Module[
  {a0, a1, alpha1, alpha0, l1, l2},
  a0 = -a[[2, 1]];
  a1 = -a[[2, 2]];
  alpha1 = -Total[poles];
  alpha0 = Times @@ poles;
  l1 = alpha1 - a1;
  l2 = alpha0 - a0 - a1 l1;
  { {l1}, {l2} }
];

lTrue = observerGain[aTrue, c, observerPoles];
lNominal = observerGain[aNominal, c, observerPoles];

Print["Matched L = ", Flatten[lTrue]];
Print["Nominal L = ", Flatten[lNominal]];
Print[
  "Matched observer-error eigenvalues = ",
  Eigenvalues[aTrue - lTrue . c]
];
Print[
  "Nominal observer-error eigenvalues = ",
  Eigenvalues[aNominal - lNominal . c]
];

u[t_] := 0.8 Sin[0.7 t] + 0.4 Sin[1.9 t];

modelData[t_, mode_String] := Which[
  mode === "matched",
  {aTrue, lTrue},
  mode === "fixed-mismatch",
  {aNominal, lNominal},
  mode === "scheduled",
  {
    aTrue + Exp[-0.35 t] (aNominal - aTrue),
    lNominal
  },
  True,
  Message[modelData::badmode, mode];
  Abort[]
];

simulate[mode_String] := Module[
  {x1, x2, xh1, xh2, aHat, l, solution},
  {aHat, l} = modelData[t, mode];
  solution = NDSolveValue[
    {
      x1'[t] == x2[t],
      x2'[t] == -2.0 x1[t] - 0.7 x2[t] + u[t],
      xh1'[t] ==
        xh2[t] + l[[1, 1]] (x1[t] - xh1[t]),
      xh2'[t] ==
        aHat[[2, 1]] xh1[t]
        + aHat[[2, 2]] xh2[t]
        + u[t]
        + l[[2, 1]] (x1[t] - xh1[t]),
      x1[0] == 1.0,
      x2[0] == -0.5,
      xh1[0] == 0.0,
      xh2[0] == 0.0
    },
    {x1, x2, xh1, xh2},
    {t, 0, 20},
    Method -> {"TimeIntegration" -> {"ExplicitRungeKutta"} }
  ];
  solution
];

matched = simulate["matched"];
fixedMismatch = simulate["fixed-mismatch"];
scheduled = simulate["scheduled"];

statePlot = Plot[
  Evaluate[
    {
      matched[[1]][t],
      matched[[3]][t],
      matched[[2]][t],
      matched[[4]][t]
    }
  ],
  {t, 0, 20},
  PlotLegends -> {"x1", "xhat1", "x2", "xhat2"},
  AxesLabel -> {"time", "state"},
  PlotLabel -> "Matched Luenberger observer",
  GridLines -> Automatic,
  ImageSize -> Large
];

errorNorm[solution_, t_] := Sqrt[
  (solution[[1]][t] - solution[[3]][t])^2
  + (solution[[2]][t] - solution[[4]][t])^2
];

errorPlot = LogPlot[
  Evaluate[
    {
      Max[errorNorm[matched, t], 10^-12],
      Max[errorNorm[fixedMismatch, t], 10^-12],
      Max[errorNorm[scheduled, t], 10^-12]
    }
  ],
  {t, 0, 20},
  PlotLegends -> {
    "matched",
    "fixed mismatch",
    "scheduled estimate"
  },
  AxesLabel -> {"time", "observer-error norm"},
  PlotLabel -> "Effect of model mismatch",
  GridLines -> Automatic,
  ImageSize -> Large
];

Print[statePlot];
Print[errorPlot];

Export[
  "Chapter15_Lesson2_mathematica_states.png",
  statePlot,
  ImageResolution -> 180
];
Export[
  "Chapter15_Lesson2_mathematica_error.png",
  errorPlot,
  ImageResolution -> 180
];

15. Observer Design and Validation Workflow

flowchart TD
  A["Choose nominal or current plant model"] --> B["Check observability of A and C"]
  B --> C["Select poles consistent with sensor and sampling bandwidth"]
  C --> D["Compute observer gain L"]
  D --> E["Verify eigenvalues and Lyapunov equation"]
  E --> F["Simulate exact-model convergence"]
  F --> G["Add parameter mismatch, noise, and initial-error sweeps"]
  G --> H["Check estimation error, control effort, and parameter-update transients"]
  H --> I["Reduce gain or add safeguards when noise or peaking is excessive"]
        

A practical validation record should include:

  • the model matrices and operating region used for design;
  • the observability rank or conditioning metric;
  • selected observer poles and the resulting gain matrix;
  • initial-condition, noise, and parameter-mismatch test cases;
  • maximum and RMS observer error;
  • sampling period and numerical solver settings;
  • the effect of estimated-state transients on control saturation and adaptation signals.

16. Problems and Solutions

Problem 1 (Second-Order Pole Placement): Consider \( A=\begin{bmatrix}0&1\\-3&-2\end{bmatrix}, C=\begin{bmatrix}1&0\end{bmatrix} \) . Find \( L \) that places the observer poles at \( -6 \) and \( -7 \).

Solution:

Here \( a_0=3 \), \( a_1=2 \), \( \omega_1=6 \), and \( \omega_2=7 \). Therefore

\[ \ell_1=6+7-2=11, \qquad \ell_2=42-3-2(11)=17. \]

Thus

\[ L=\begin{bmatrix}11\\17\end{bmatrix}. \]

Verification gives

\[ \det\!\bigl(sI-(A-LC)\bigr) =s^2+13s+42=(s+6)(s+7). \]


Problem 2 (Lyapunov Convergence Bound): Assume \( F=A-LC \) is Hurwitz and \( P,Q\succ0 \) satisfy \( F^{T}P+PF=-Q \). Derive an exponential bound for \( \lVert e_o(t)\rVert \).

Solution:

With \( V=e_o^{T}Pe_o \),

\[ \dot{V}=-e_o^{T}Qe_o \leq-\lambda_{\min}(Q)\lVert e_o\rVert^2 \leq -\frac{\lambda_{\min}(Q)}{\lambda_{\max}(P)}V. \]

The comparison lemma gives

\[ V(t) \leq V(0)e^{-ct}, \qquad c=\frac{\lambda_{\min}(Q)}{\lambda_{\max}(P)}. \]

Combining the upper and lower eigenvalue bounds on \( V \) gives

\[ \lVert e_o(t)\rVert \leq \sqrt{\frac{\lambda_{\max}(P)}{\lambda_{\min}(P)} } e^{-ct/2}\lVert e_o(0)\rVert. \]


Problem 3 (Constant Model Mismatch): Let \( \dot{e}_o=Fe_o+d_0 \), where \( F \) is Hurwitz and \( d_0 \) is a constant nonzero vector. Determine the equilibrium observer error.

Solution:

At equilibrium, \( 0=Fe_{o,\mathrm{ss} }+d_0 \). Since a Hurwitz matrix is nonsingular,

\[ \boxed{ e_{o,\mathrm{ss} }=-F^{-1}d_0. } \]

Thus stable observer poles guarantee boundedness and convergence to an equilibrium, but not convergence to zero when a persistent mismatch forcing remains.


Problem 4 (Noise-to-Error Transfer): For \( \dot{e}_o=Fe_o-Ln \), derive the transfer matrix from sensor noise to observer error.

Solution:

Taking Laplace transforms with zero initial error,

\[ sE_o(s)=FE_o(s)-LN(s). \]

Therefore

\[ \boxed{ E_o(s)=-(sI-F)^{-1}L\,N(s). } \]

Increasing the observer bandwidth moves the poles of \( (sI-F)^{-1} \) leftward, but often increases \( \lVert L\rVert \). The net high-frequency noise sensitivity must be evaluated rather than inferred from pole speed alone.


Problem 5 (Forward-Euler Stability): A scalar continuous observer error obeys \( \dot{e}_o=-\lambda e_o \), where \( \lambda>0 \). It is implemented by forward Euler with sampling period \( T_s \). Find the numerical-stability condition.

Solution:

The Euler update is

\[ e_{o,k+1}=(1-\lambda T_s)e_{o,k}. \]

Discrete-time stability requires

\[ |1-\lambda T_s|<1. \]

Solving the inequality gives

\[ \boxed{ 0<T_s<\frac{2}{\lambda}. } \]

Accuracy normally requires a much smaller period than this limiting stability condition.


Problem 6 (Observer Error in a Controller Regressor): Suppose \( u=\hat{K}^{T}\hat{x} \), while the ideal state-based signal is \( u_{\star}=\hat{K}^{T}x \). Express and bound the control perturbation.

Solution:

\[ \delta u=u-u_{\star} =\hat{K}^{T}(\hat{x}-x) =-\hat{K}^{T}e_o. \]

By the Cauchy–Schwarz inequality,

\[ \boxed{ |\delta u| \leq \lVert\hat{K}\rVert\lVert e_o\rVert. } \]

Projection or another parameter-bounding mechanism limits \( \lVert\hat{K}\rVert \), while observer design limits \( \lVert e_o\rVert \). Both mechanisms are therefore relevant to bounding the output-feedback perturbation.

17. Summary

A full-order Luenberger observer reconstructs the state by combining a model prediction with an innovation correction. For an exact, observable linear model, choosing \( A-LC \) Hurwitz yields global exponential convergence, as shown by pole placement and a quadratic Lyapunov proof. In adaptive use, parameter mismatch appears as an external forcing in the observer-error dynamics, while sensor noise enters through \( -Ln \). Faster observer poles reduce nominal transients but can increase noise sensitivity and peaking. Most importantly, inserting an observer into an adaptive loop creates nonlinear and time-varying coupling, so nominal observer and controller stability cannot be treated as an automatic separation result.

18. References

  1. Luenberger, D.G. (1964). Observing the state of a linear system. IEEE Transactions on Military Electronics, 8(2), 74–80.
  2. Luenberger, D.G. (1966). Observers for multivariable systems. IEEE Transactions on Automatic Control, 11(2), 190–197.
  3. Carroll, R.L., & Lindorff, D.P. (1973). An adaptive observer for single-input single-output linear systems. IEEE Transactions on Automatic Control, 18(5), 428–435.
  4. Lüders, G., & Narendra, K.S. (1974). Stable adaptive schemes for state estimation and identification of linear systems. IEEE Transactions on Automatic Control, 19(6), 841–847.
  5. Kreisselmeier, G. (1977). Adaptive observers with exponential rate of convergence. IEEE Transactions on Automatic Control, 22(1), 2–8.
  6. Bastin, G., & Gevers, M. (1988). Stable adaptive observers for nonlinear time-varying systems. IEEE Transactions on Automatic Control, 33(7), 650–658.
  7. Marino, R., & Tomei, P. (1992). Global adaptive observers for nonlinear systems via filtered transformations. IEEE Transactions on Automatic Control, 37(8), 1239–1245.
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.