Chapter 9: Parameter Projection and Normalization Techniques

Lesson 4: Normalization of Signals and Regressors

This lesson develops normalization as a scale-management mechanism for adaptive laws. Starting from the linear parametric error model, we derive static and dynamic normalizers, prove bounded normalized regressors, establish continuous- and discrete-time stability inequalities, and show how normalization can be combined with the projection operator introduced in the preceding lessons. Numerical implementations in Python, C++, Java, MATLAB/Simulink, and Wolfram Mathematica compare raw and normalized gradient adaptation during a sudden regressor-amplitude burst.

1. Learning Objectives and Prerequisite Context

After completing this lesson, students should be able to:

  • explain why a fixed adaptation gain is sensitive to regressor scaling;
  • derive normalized continuous- and discrete-time gradient laws;
  • prove the main boundedness and Lyapunov properties of normalization;
  • distinguish normalization from projection, clipping, and statistical standardization;
  • select the regularization constant and adaptation gain systematically;
  • implement a normalized adaptive law and diagnose numerical failure of an unnormalized law.

We use concepts already introduced in earlier chapters: linearly parameterized models, prediction or tracking error, gradient adaptation, Lyapunov functions, robust modifications, and projection onto an admissible parameter set. Persistent excitation is intentionally not required here; it is introduced in Chapter 10.

2. Why Raw Gradient Adaptation is Scale Sensitive

Consider the scalar linear regression model \( y(t)=\theta^{*T}\phi(t) \), where \( \theta^*\in\mathbb{R}^p \) is constant and unknown, and \( \phi(t)\in\mathbb{R}^p \) is measurable. Let

\[ \hat y(t)=\hat\theta^T(t)\phi(t),\qquad \varepsilon(t)=y(t)-\hat y(t)=\tilde\theta^T(t)\phi(t),\qquad \tilde\theta(t)=\theta^*-\hat\theta(t). \]

The ordinary gradient law is

\[ \dot{\hat\theta}=\Gamma\phi\varepsilon,\qquad \Gamma=\Gamma^T > 0. \]

Its update magnitude satisfies

\[ \|\dot{\hat\theta}\| \leq \|\Gamma\|\,\|\phi\|\,|\varepsilon|. \]

Since the ideal error is \( |\varepsilon|\leq\|\tilde\theta\|\|\phi\| \), the raw update can grow quadratically with regressor magnitude:

\[ \|\dot{\hat\theta}\| \leq \|\Gamma\|\,\|\tilde\theta\|\,\|\phi\|^2. \]

Therefore, a gain that is reasonable when \( \|\phi\|\approx 1 \) can become numerically aggressive when the same physical experiment produces \( \|\phi\|\approx 10 \) or \( \|\phi\|\approx 100 \). The issue is particularly visible in sampled implementations, where one large update can move the estimate far from its admissible region before the next sample arrives.

flowchart TD
  A["Measured signals"] --> B["Build regressor phi"]
  B --> C["Compute prediction y_hat = theta_hat^T phi"]
  C --> D["Prediction error e = y - y_hat"]
  D --> E["Raw update magnitude proportional to norm(phi) times abs(e)"]
  E --> F{"Regressor amplitude \nchanges?"}
  F -->|"small"| G["Moderate parameter step"]
  F -->|"large"| H["Potentially excessive parameter step"]
  H --> I["Use a positive normalizer before updating"]
        

3. Static Euclidean Normalization

A standard static normalizing signal is defined by

\[ m^2(t)=\delta+\phi^T(t)\phi(t),\qquad \delta > 0. \]

The strictly positive constant \( \delta \) prevents division by zero and limits amplification when the regressor is small. Define the normalized regressor and normalized error as

\[ \phi_N(t)=\frac{\phi(t)}{m(t)},\qquad \varepsilon_N(t)=\frac{\varepsilon(t)}{m(t)}. \]

The normalized gradient law is then

\[ \boxed{\dot{\hat\theta} =\Gamma\phi_N\varepsilon_N =\Gamma\frac{\phi\varepsilon}{\delta+\phi^T\phi}}. \]

This form normalizes the product driving the adaptive law. Dividing only the regressor by \( m \) would produce a denominator \( m \), not \( m^2 \), and would not provide the same discrete-time scale invariance or stability bound.

A gain-weighted alternative is useful when \( \Gamma \) is strongly anisotropic:

\[ m_\Gamma^2(t)=\delta+\phi^T(t)\Gamma\phi(t),\qquad \dot{\hat\theta}=\Gamma\frac{\phi\varepsilon}{m_\Gamma^2}. \]

The Euclidean form is easier to interpret and is used throughout the implementations in this lesson. The metric-aware form can yield a tighter step-size condition when the parameter coordinates have very different adaptation gains.

4. Fundamental Properties of the Normalizer

4.1 Bounded normalized regressor

Because \( m^2=\delta+\|\phi\|^2 \),

\[ \|\phi_N\|^2 =\frac{\|\phi\|^2}{\delta+\|\phi\|^2} < 1. \]

Thus the normalized regressor belongs to the open unit ball for every finite regressor and every \( \delta > 0 \).

4.2 Bound on the ideal normalized update

Under the exact parametric model,

\[ \left\|\frac{\phi\varepsilon}{\delta+\|\phi\|^2}\right\| =\frac{\|\phi\|\,|\tilde\theta^T\phi|} {\delta+\|\phi\|^2} \leq \|\tilde\theta\|\frac{\|\phi\|^2} {\delta+\|\phi\|^2} < \|\tilde\theta\|. \]

Consequently,

\[ \|\dot{\hat\theta}\| < \|\Gamma\|\,\|\tilde\theta\|. \]

This does not assert that the parameter error is known or that the update is globally bounded by a known numerical constant. It shows that the update no longer contains an explicit quadratic growth factor in \( \|\phi\| \).

4.3 Approximate scale invariance

Suppose the physical units or sensor gain scale both the regression data by a constant \( c\neq 0 \):

\[ \phi'=c\phi,\qquad y'=cy,\qquad \varepsilon'=c\varepsilon. \]

The normalized direction becomes

\[ \frac{\phi'\varepsilon'}{\delta+\phi'^T\phi'} =\frac{c^2\phi\varepsilon}{\delta+c^2\phi^T\phi}. \]

When \( c^2\|\phi\|^2\gg\delta \), this is approximately \( \phi\varepsilon/(\phi^T\phi) \), independent of \( c \). The constant \( \delta \) intentionally breaks exact invariance near zero to regularize the update.

5. Continuous-Time Lyapunov Analysis

Assume the exact regression model and choose the normalized law

\[ \dot{\hat\theta}=\Gamma \frac{\phi\varepsilon}{m^2},\qquad m^2=\delta+\phi^T\phi. \]

Since \( \tilde\theta=\theta^*-\hat\theta \) and \( \theta^* \) is constant,

\[ \dot{\tilde\theta}=-\Gamma \frac{\phi\phi^T\tilde\theta}{m^2}. \]

Consider the quadratic Lyapunov function

\[ V(\tilde\theta)=\frac{1}{2} \tilde\theta^T\Gamma^{-1}\tilde\theta. \]

Its derivative is

\[ \begin{aligned} \dot V &=\tilde\theta^T\Gamma^{-1}\dot{\tilde\theta} \\ &=-\tilde\theta^T\phi\frac{\phi^T\tilde\theta}{m^2} \\ &=-\frac{\varepsilon^2}{m^2} =-\varepsilon_N^2\leq 0. \end{aligned} \]

Therefore \( V(t) \) is nonincreasing and \( \tilde\theta(t) \) remains bounded. Moreover,

\[ \int_0^T \varepsilon_N^2(t)\,dt =V(0)-V(T)\leq V(0), \]

so \( \varepsilon_N\in L_2 \). This proof establishes boundedness and energy dissipation of the normalized prediction error. It does not by itself prove convergence of every component of \( \hat\theta \) to \( \theta^* \); that stronger question requires an excitation condition developed in Chapter 10.

Notice that the unnormalized ideal gradient law also yields \( \dot V=-\varepsilon^2 \). The principal advantages of normalization are scale moderation, improved sampled-data behavior, and better control of update magnitudes—not a fundamentally different nominal continuous-time Lyapunov sign.

6. Discrete-Time Normalized Gradient Law and Stability Range

For sampled data, use

\[ \hat\theta_{k+1}=\hat\theta_k+ \mu\frac{\phi_k\varepsilon_k} {\delta+\phi_k^T\phi_k},\qquad \varepsilon_k=\tilde\theta_k^T\phi_k. \]

Let \( V_k=\|\tilde\theta_k\|^2 \) and define

\[ q_k=\frac{\phi_k^T\phi_k} {\delta+\phi_k^T\phi_k},\qquad 0\leq q_k < 1. \]

The parameter-error recursion is

\[ \tilde\theta_{k+1}=\tilde\theta_k- \mu\frac{\phi_k\phi_k^T\tilde\theta_k}{m_k^2}. \]

Expanding the Lyapunov difference gives

\[ \begin{aligned} V_{k+1}-V_k &=-2\mu\frac{\varepsilon_k^2}{m_k^2} +\mu^2\frac{\varepsilon_k^2\phi_k^T\phi_k}{m_k^4} \\ &=-\mu\left(2-\mu q_k\right) \frac{\varepsilon_k^2}{m_k^2}. \end{aligned} \]

Since \( q_k < 1 \), the scale-independent sufficient condition

\[ \boxed{0 < \mu < 2} \]

implies

\[ V_{k+1}-V_k \leq-\mu(2-\mu) \frac{\varepsilon_k^2}{m_k^2}\leq 0. \]

By contrast, the raw update \( \hat\theta_{k+1}=\hat\theta_k+\mu\phi_k\varepsilon_k \) requires the sample-dependent restriction \( 0 < \mu\|\phi_k\|^2 < 2 \). A sudden increase in regressor energy can therefore invalidate a previously safe fixed gain.

7. Dynamic Normalizers and Alternative Choices

7.1 First-order dynamic normalizer

A dynamic normalizer smooths short regressor spikes by filtering signal energy:

\[ \dot n=-\lambda_n n+\phi^T\phi,\qquad m^2=\delta+n,\qquad \lambda_n > 0. \]

With \( n(0)\geq 0 \), the solution is

\[ n(t)=e^{-\lambda_n t}n(0)+ \int_0^t e^{-\lambda_n(t-\tau)}\|\phi(\tau)\|^2d\tau\geq 0, \]

so \( m^2(t)\geq\delta \). Small \( \lambda_n \) gives longer memory and stronger smoothing; large \( \lambda_n \) approaches an instantaneous energy estimate.

7.2 Component scaling before norm normalization

When regressor components have different physical units, first introduce a known nonsingular scale matrix \( S \) and define

\[ \bar\phi=S^{-1}\phi,\qquad m^2=\delta+\bar\phi^T\bar\phi. \]

This preserves the vector direction in dimensionless coordinates. Independently dividing each component by its current absolute value is generally undesirable because it changes relative directional information and becomes singular whenever a component crosses zero.

7.3 Saturated and bounded normalizers

In finite-precision implementations, one may impose

\[ m^2_{\mathrm{used}}= \min\!\left(m_{\max}^2, \max\!\left(m_{\min}^2,\delta+\phi^T\phi\right)\right), \]

where \( 0 < m_{\min}^2 < m_{\max}^2 \). The lower bound protects division; the upper bound prevents overflow in the normalizer computation. However, an upper saturation partially removes normalization during extreme signal bursts, so it must be selected above the expected operating range.

8. Combining Normalization with Parameter Projection

Let \( \Omega \) be the closed convex parameter set introduced in Lessons 1–3 of this chapter, and assume \( \theta^*\in\Omega \). A normalized projected law is

\[ \dot{\hat\theta} =\operatorname{Proj}_{\Omega}\!\left( \hat\theta, \Gamma\frac{\phi\varepsilon}{m^2} \right). \]

Denote the unprojected update direction by \( v=\Gamma\phi\varepsilon/m^2 \). The projection property established previously gives

\[ \tilde\theta^T\Gamma^{-1} \left(\operatorname{Proj}_{\Omega}(\hat\theta,v)-v\right) \geq 0, \]

because this lesson uses \( \tilde\theta=\theta^*-\hat\theta \). Therefore,

\[ \begin{aligned} \dot V &=-\tilde\theta^T\Gamma^{-1} \operatorname{Proj}_{\Omega}(\hat\theta,v) \\ &\leq-\tilde\theta^T\Gamma^{-1}v =-\frac{\varepsilon^2}{m^2}\leq 0. \end{aligned} \]

Projection and normalization solve different problems. Projection enforces parameter constraints; normalization moderates the signal-dependent update scale. They are complementary and are often used together.

9. Implementation Sequence

flowchart TD
  A["Acquire y and signals used in phi"] --> B["Construct phi in consistent physical units"]
  B --> C["Compute y_hat = theta_hat^T phi"]
  C --> D["Compute error e = y - y_hat"]
  D --> E["Compute m2 = delta + phi^T phi"]
  E --> F["Candidate update v = Gamma phi e / m2"]
  F --> G{"Parameter constraints \nenabled?"}
  G -->|"no"| H["Apply theta_hat next = theta_hat + step"]
  G -->|"yes"| I["Apply projected normalized step"]
  I --> H
  H --> J["Log error, m2, update norm, and parameter estimate"]
  J --> A
        

A robust implementation should additionally check:

  • finite inputs: reject NaN or infinite measurements before forming the update;
  • strictly positive denominator: enforce \( \delta > 0 \) and a numerical lower bound;
  • consistent sampling: include the sample period in the gain when discretizing a continuous law;
  • diagnostic logging: record \( \|\phi\| \), \( m^2 \), \( \|\Delta\hat\theta\| \), and projection activity;
  • actuator safety: normalization does not replace the saturation and safety logic discussed in earlier lessons.

10. Software Libraries and Numerical Experiment

The experiment uses the exact regression \( y_k=\theta^{*T}\phi_k+v_k \) with \( \theta^*=[2,-1]^T \). The second regressor component increases by a factor of ten between samples 500 and 849. The raw estimator uses a fixed gain selected for ordinary signal levels, whereas the normalized estimator uses \( \mu=0.8 \) and \( \delta=0.1 \).

  • Python: NumPy supplies vector operations and Matplotlib supplies plots. The control package can simulate linear plants, but the adaptive update is normally implemented explicitly.
  • C++: the example uses only the standard library. Eigen is a common alternative for larger parameter vectors and matrix-valued adaptation gains.
  • Java: the example uses primitive arrays and standard I/O. Apache Commons Math can replace the helper vector operations in larger projects.
  • MATLAB/Simulink: native matrix syntax directly implements the law. In Simulink, a MATLAB Function block, Dot Product block, Sum block, Gain block, and Unit Delay block are sufficient for a discrete normalized update.
  • Wolfram Mathematica: built-in list algebra, ListLinePlot, and Export provide a compact implementation without external packages.

The code deliberately marks the raw estimate as diverged once its norm exceeds \( 10^6 \), but continues running the normalized estimator so that its behavior remains observable.

11. Python Implementation

Chapter9_Lesson4.py

"""Chapter 9, Lesson 4: Normalization of Signals and Regressors.

Compares an unnormalized gradient estimator with a normalized gradient
estimator when the regressor magnitude changes abruptly.
"""

from __future__ import annotations

import math
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


N_SAMPLES = 1600
TRUE_THETA = np.array([2.0, -1.0], dtype=float)
MU_RAW = 0.04
MU_NORMALIZED = 0.8
DELTA = 0.1
DIVERGENCE_LIMIT = 1.0e6
OUTPUT_CSV = Path("Chapter9_Lesson4_results.csv")
OUTPUT_PNG = Path("Chapter9_Lesson4_results.png")


def regressor(sample: int) -> np.ndarray:
    """Return a two-component regressor with a temporary amplitude burst."""
    if sample < 500:
        amplitude = 1.0
    elif sample < 850:
        amplitude = 10.0
    else:
        amplitude = 1.5

    return np.array(
        [
            math.sin(0.037 * sample) + 0.5 * math.cos(0.011 * sample),
            amplitude * math.cos(0.023 * sample),
        ],
        dtype=float,
    )


def normalized_gradient_step(
    theta_hat: np.ndarray,
    phi: np.ndarray,
    prediction_error: float,
    step_size: float,
    delta: float,
) -> np.ndarray:
    """Perform one Euclidean normalized-gradient update."""
    denominator = delta + float(phi @ phi)
    return theta_hat + step_size * phi * prediction_error / denominator


def simulate() -> dict[str, np.ndarray | int | None]:
    theta_raw = np.zeros(2, dtype=float)
    theta_normalized = np.zeros(2, dtype=float)
    raw_diverged_at: int | None = None

    raw_history = np.full((N_SAMPLES, 2), np.nan, dtype=float)
    normalized_history = np.zeros((N_SAMPLES, 2), dtype=float)
    raw_error_history = np.full(N_SAMPLES, np.nan, dtype=float)
    normalized_error_history = np.zeros(N_SAMPLES, dtype=float)
    regressor_norm_history = np.zeros(N_SAMPLES, dtype=float)
    normalizer_history = np.zeros(N_SAMPLES, dtype=float)

    for sample in range(N_SAMPLES):
        phi = regressor(sample)
        noise = 0.01 * math.sin(0.071 * sample)
        measured_output = float(TRUE_THETA @ phi) + noise

        normalized_error = measured_output - float(theta_normalized @ phi)
        theta_normalized = normalized_gradient_step(
            theta_normalized,
            phi,
            normalized_error,
            MU_NORMALIZED,
            DELTA,
        )

        if raw_diverged_at is None:
            raw_error = measured_output - float(theta_raw @ phi)
            theta_raw = theta_raw + MU_RAW * phi * raw_error
            raw_error_history[sample] = raw_error

            if (
                not np.all(np.isfinite(theta_raw))
                or np.linalg.norm(theta_raw) > DIVERGENCE_LIMIT
            ):
                raw_diverged_at = sample
            else:
                raw_history[sample] = theta_raw

        normalized_history[sample] = theta_normalized
        normalized_error_history[sample] = normalized_error
        regressor_norm_history[sample] = np.linalg.norm(phi)
        normalizer_history[sample] = DELTA + float(phi @ phi)

    return {
        "raw_history": raw_history,
        "normalized_history": normalized_history,
        "raw_error_history": raw_error_history,
        "normalized_error_history": normalized_error_history,
        "regressor_norm_history": regressor_norm_history,
        "normalizer_history": normalizer_history,
        "raw_diverged_at": raw_diverged_at,
    }


def save_results(results: dict[str, np.ndarray | int | None]) -> None:
    samples = np.arange(N_SAMPLES)
    raw_history = np.asarray(results["raw_history"])
    normalized_history = np.asarray(results["normalized_history"])
    raw_error_history = np.asarray(results["raw_error_history"])
    normalized_error_history = np.asarray(results["normalized_error_history"])
    regressor_norm_history = np.asarray(results["regressor_norm_history"])
    normalizer_history = np.asarray(results["normalizer_history"])

    table = np.column_stack(
        [
            samples,
            regressor_norm_history,
            normalizer_history,
            raw_history[:, 0],
            raw_history[:, 1],
            normalized_history[:, 0],
            normalized_history[:, 1],
            raw_error_history,
            normalized_error_history,
        ]
    )
    np.savetxt(
        OUTPUT_CSV,
        table,
        delimiter=",",
        header=(
            "sample,regressor_norm,normalizer,raw_theta_1,raw_theta_2,"
            "normalized_theta_1,normalized_theta_2,raw_error,normalized_error"
        ),
        comments="",
    )

    plt.figure(figsize=(10, 6))
    plt.plot(samples, normalized_history[:, 0], label="normalized theta_1")
    plt.plot(samples, normalized_history[:, 1], label="normalized theta_2")
    plt.plot(samples, raw_history[:, 0], "--", label="raw theta_1")
    plt.plot(samples, raw_history[:, 1], "--", label="raw theta_2")
    plt.axhline(TRUE_THETA[0], linestyle=":", label="true theta_1")
    plt.axhline(TRUE_THETA[1], linestyle=":", label="true theta_2")
    plt.axvspan(500, 850, alpha=0.12, label="regressor-amplitude burst")
    plt.xlabel("sample")
    plt.ylabel("parameter estimate")
    plt.title("Raw and normalized gradient adaptation")
    plt.grid(True)
    plt.legend(loc="best")
    plt.tight_layout()
    plt.savefig(OUTPUT_PNG, dpi=160)
    plt.show()


def main() -> None:
    results = simulate()
    normalized_history = np.asarray(results["normalized_history"])
    raw_diverged_at = results["raw_diverged_at"]

    print("True parameter:", TRUE_THETA)
    print("Final normalized estimate:", normalized_history[-1])
    if raw_diverged_at is None:
        raw_history = np.asarray(results["raw_history"])
        print("Final raw estimate:", raw_history[-1])
    else:
        print(f"Raw estimator exceeded {DIVERGENCE_LIMIT:g} at sample {raw_diverged_at}.")

    save_results(results)
    print(f"Saved {OUTPUT_CSV} and {OUTPUT_PNG}.")


if __name__ == "__main__":
    main()

12. C++ Implementation

Chapter9_Lesson4.cpp

// Chapter 9, Lesson 4: Normalization of Signals and Regressors.
// Compares raw and normalized gradient estimators under a regressor burst.

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

namespace {
constexpr int kSamples = 1600;
constexpr double kMuRaw = 0.04;
constexpr double kMuNormalized = 0.8;
constexpr double kDelta = 0.1;
constexpr double kDivergenceLimit = 1.0e6;
constexpr std::array<double, 2> kTrueTheta{2.0, -1.0};

using Vector2 = std::array<double, 2>;

double dot(const Vector2& a, const Vector2& b) {
    return a[0] * b[0] + a[1] * b[1];
}

double norm(const Vector2& value) {
    return std::sqrt(dot(value, value));
}

Vector2 regressor(int sample) {
    double amplitude = 1.0;
    if (sample >= 500 && sample < 850) {
        amplitude = 10.0;
    } else if (sample >= 850) {
        amplitude = 1.5;
    }

    return {
        std::sin(0.037 * sample) + 0.5 * std::cos(0.011 * sample),
        amplitude * std::cos(0.023 * sample),
    };
}

Vector2 normalizedGradientStep(
    const Vector2& thetaHat,
    const Vector2& phi,
    double predictionError,
    double stepSize,
    double delta) {
    const double denominator = delta + dot(phi, phi);
    return {
        thetaHat[0] + stepSize * phi[0] * predictionError / denominator,
        thetaHat[1] + stepSize * phi[1] * predictionError / denominator,
    };
}
}  // namespace

int main() {
    Vector2 thetaRaw{0.0, 0.0};
    Vector2 thetaNormalized{0.0, 0.0};
    bool rawDiverged = false;
    int rawDivergedAt = -1;

    std::ofstream csv("Chapter9_Lesson4_results_cpp.csv");
    if (!csv) {
        throw std::runtime_error("Unable to create output CSV file.");
    }
    csv << "sample,regressor_norm,normalizer,raw_theta_1,raw_theta_2,"
           "normalized_theta_1,normalized_theta_2,raw_error,normalized_error\n";
    csv << std::setprecision(15);

    for (int sample = 0; sample < kSamples; ++sample) {
        const Vector2 phi = regressor(sample);
        const double noise = 0.01 * std::sin(0.071 * sample);
        const double measuredOutput = dot(kTrueTheta, phi) + noise;

        const double normalizedError = measuredOutput - dot(thetaNormalized, phi);
        thetaNormalized = normalizedGradientStep(
            thetaNormalized, phi, normalizedError, kMuNormalized, kDelta);

        double rawError = std::numeric_limits<double>::quiet_NaN();
        if (!rawDiverged) {
            rawError = measuredOutput - dot(thetaRaw, phi);
            thetaRaw[0] += kMuRaw * phi[0] * rawError;
            thetaRaw[1] += kMuRaw * phi[1] * rawError;
            if (!std::isfinite(thetaRaw[0]) || !std::isfinite(thetaRaw[1]) ||
                norm(thetaRaw) > kDivergenceLimit) {
                rawDiverged = true;
                rawDivergedAt = sample;
            }
        }

        const double rawTheta1 = rawDiverged
            ? std::numeric_limits<double>::quiet_NaN()
            : thetaRaw[0];
        const double rawTheta2 = rawDiverged
            ? std::numeric_limits<double>::quiet_NaN()
            : thetaRaw[1];
        const double normalizer = kDelta + dot(phi, phi);

        csv << sample << ',' << norm(phi) << ',' << normalizer << ','
            << rawTheta1 << ',' << rawTheta2 << ','
            << thetaNormalized[0] << ',' << thetaNormalized[1] << ','
            << rawError << ',' << normalizedError << '\n';
    }

    std::cout << "True parameter: [" << kTrueTheta[0] << ", "
              << kTrueTheta[1] << "]\n";
    std::cout << "Final normalized estimate: [" << thetaNormalized[0] << ", "
              << thetaNormalized[1] << "]\n";
    if (rawDiverged) {
        std::cout << "Raw estimator exceeded " << kDivergenceLimit
                  << " at sample " << rawDivergedAt << ".\n";
    } else {
        std::cout << "Final raw estimate: [" << thetaRaw[0] << ", "
                  << thetaRaw[1] << "]\n";
    }
    std::cout << "Saved Chapter9_Lesson4_results_cpp.csv.\n";
    return 0;
}

13. Java Implementation

Chapter9_Lesson4.java

// Chapter 9, Lesson 4: Normalization of Signals and Regressors.
// Compares raw and normalized gradient estimators under a regressor burst.

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.Locale;

public final class Chapter9_Lesson4 {
    private static final int SAMPLES = 1600;
    private static final double MU_RAW = 0.04;
    private static final double MU_NORMALIZED = 0.8;
    private static final double DELTA = 0.1;
    private static final double DIVERGENCE_LIMIT = 1.0e6;
    private static final double[] TRUE_THETA = {2.0, -1.0};

    private Chapter9_Lesson4() {
    }

    private static double dot(double[] a, double[] b) {
        return a[0] * b[0] + a[1] * b[1];
    }

    private static double norm(double[] value) {
        return Math.sqrt(dot(value, value));
    }

    private static double[] regressor(int sample) {
        final double amplitude;
        if (sample < 500) {
            amplitude = 1.0;
        } else if (sample < 850) {
            amplitude = 10.0;
        } else {
            amplitude = 1.5;
        }

        return new double[] {
            Math.sin(0.037 * sample) + 0.5 * Math.cos(0.011 * sample),
            amplitude * Math.cos(0.023 * sample)
        };
    }

    private static double[] normalizedGradientStep(
            double[] thetaHat,
            double[] phi,
            double predictionError,
            double stepSize,
            double delta) {
        final double denominator = delta + dot(phi, phi);
        return new double[] {
            thetaHat[0] + stepSize * phi[0] * predictionError / denominator,
            thetaHat[1] + stepSize * phi[1] * predictionError / denominator
        };
    }

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

        double[] thetaRaw = {0.0, 0.0};
        double[] thetaNormalized = {0.0, 0.0};
        boolean rawDiverged = false;
        int rawDivergedAt = -1;

        final Path output = Path.of("Chapter9_Lesson4_results_java.csv");
        try (BufferedWriter csv = Files.newBufferedWriter(
                output, StandardCharsets.UTF_8)) {
            csv.write("sample,regressor_norm,normalizer,raw_theta_1,raw_theta_2,");
            csv.write("normalized_theta_1,normalized_theta_2,raw_error,normalized_error\n");

            for (int sample = 0; sample < SAMPLES; sample++) {
                final double[] phi = regressor(sample);
                final double noise = 0.01 * Math.sin(0.071 * sample);
                final double measuredOutput = dot(TRUE_THETA, phi) + noise;

                final double normalizedError = measuredOutput - dot(thetaNormalized, phi);
                thetaNormalized = normalizedGradientStep(
                    thetaNormalized, phi, normalizedError, MU_NORMALIZED, DELTA);

                double rawError = Double.NaN;
                if (!rawDiverged) {
                    rawError = measuredOutput - dot(thetaRaw, phi);
                    thetaRaw[0] += MU_RAW * phi[0] * rawError;
                    thetaRaw[1] += MU_RAW * phi[1] * rawError;
                    if (!Double.isFinite(thetaRaw[0])
                            || !Double.isFinite(thetaRaw[1])
                            || norm(thetaRaw) > DIVERGENCE_LIMIT) {
                        rawDiverged = true;
                        rawDivergedAt = sample;
                    }
                }

                final double rawTheta1 = rawDiverged ? Double.NaN : thetaRaw[0];
                final double rawTheta2 = rawDiverged ? Double.NaN : thetaRaw[1];
                final double normalizer = DELTA + dot(phi, phi);

                csv.write(String.format(
                    Locale.ROOT,
                    "%d,%.15g,%.15g,%.15g,%.15g,%.15g,%.15g,%.15g,%.15g%n",
                    sample,
                    norm(phi),
                    normalizer,
                    rawTheta1,
                    rawTheta2,
                    thetaNormalized[0],
                    thetaNormalized[1],
                    rawError,
                    normalizedError));
            }
        }

        System.out.printf("True parameter: [%.6f, %.6f]%n", TRUE_THETA[0], TRUE_THETA[1]);
        System.out.printf(
            "Final normalized estimate: [%.6f, %.6f]%n",
            thetaNormalized[0], thetaNormalized[1]);
        if (rawDiverged) {
            System.out.printf(
                "Raw estimator exceeded %.1e at sample %d.%n",
                DIVERGENCE_LIMIT, rawDivergedAt);
        } else {
            System.out.printf(
                "Final raw estimate: [%.6f, %.6f]%n",
                thetaRaw[0], thetaRaw[1]);
        }
        System.out.println("Saved " + output + ".");
    }
}

14. MATLAB/Simulink Implementation

Chapter9_Lesson4.m

%% Chapter 9, Lesson 4: Normalization of Signals and Regressors
% Compares raw and normalized gradient estimators under a regressor burst.
% The local function normalizedUpdateStep can also be copied into a
% MATLAB Function block in Simulink.

clear; clc; close all;

nSamples = 1600;
trueTheta = [2.0; -1.0];
muRaw = 0.04;
muNormalized = 0.8;
delta = 0.1;
divergenceLimit = 1.0e6;

thetaRaw = zeros(2, 1);
thetaNormalized = zeros(2, 1);
rawDiverged = false;
rawDivergedAt = NaN;

rawHistory = NaN(2, nSamples);
normalizedHistory = zeros(2, nSamples);
rawErrorHistory = NaN(1, nSamples);
normalizedErrorHistory = zeros(1, nSamples);
regressorNormHistory = zeros(1, nSamples);
normalizerHistory = zeros(1, nSamples);

for sample = 0:(nSamples - 1)
    if sample < 500
        amplitude = 1.0;
    elseif sample < 850
        amplitude = 10.0;
    else
        amplitude = 1.5;
    end

    phi = [sin(0.037 * sample) + 0.5 * cos(0.011 * sample); ...
           amplitude * cos(0.023 * sample)];
    noise = 0.01 * sin(0.071 * sample);
    measuredOutput = trueTheta.' * phi + noise;

    normalizedError = measuredOutput - thetaNormalized.' * phi;
    thetaNormalized = normalizedUpdateStep( ...
        thetaNormalized, phi, normalizedError, muNormalized, delta);

    if ~rawDiverged
        rawError = measuredOutput - thetaRaw.' * phi;
        thetaRaw = thetaRaw + muRaw * phi * rawError;
        rawErrorHistory(sample + 1) = rawError;

        if any(~isfinite(thetaRaw)) || norm(thetaRaw) > divergenceLimit
            rawDiverged = true;
            rawDivergedAt = sample;
        else
            rawHistory(:, sample + 1) = thetaRaw;
        end
    end

    normalizedHistory(:, sample + 1) = thetaNormalized;
    normalizedErrorHistory(sample + 1) = normalizedError;
    regressorNormHistory(sample + 1) = norm(phi);
    normalizerHistory(sample + 1) = delta + phi.' * phi;
end

sampleVector = (0:(nSamples - 1)).';
results = table( ...
    sampleVector, ...
    regressorNormHistory.', ...
    normalizerHistory.', ...
    rawHistory(1, :).', ...
    rawHistory(2, :).', ...
    normalizedHistory(1, :).', ...
    normalizedHistory(2, :).', ...
    rawErrorHistory.', ...
    normalizedErrorHistory.', ...
    'VariableNames', { ...
        'sample', 'regressor_norm', 'normalizer', ...
        'raw_theta_1', 'raw_theta_2', ...
        'normalized_theta_1', 'normalized_theta_2', ...
        'raw_error', 'normalized_error'});
writetable(results, 'Chapter9_Lesson4_results_matlab.csv');

figure('Name', 'Raw and normalized gradient adaptation');
plot(sampleVector, normalizedHistory(1, :), 'LineWidth', 1.2); hold on;
plot(sampleVector, normalizedHistory(2, :), 'LineWidth', 1.2);
plot(sampleVector, rawHistory(1, :), '--');
plot(sampleVector, rawHistory(2, :), '--');
yline(trueTheta(1), ':');
yline(trueTheta(2), ':');
xline(500, '-.');
xline(850, '-.');
grid on;
xlabel('sample');
ylabel('parameter estimate');
title('Raw and normalized gradient adaptation');
legend( ...
    'normalized theta_1', 'normalized theta_2', ...
    'raw theta_1', 'raw theta_2', ...
    'true theta_1', 'true theta_2', ...
    'burst begins', 'burst ends', ...
    'Location', 'best');

fprintf('True parameter: [%.6f, %.6f]\n', trueTheta(1), trueTheta(2));
fprintf('Final normalized estimate: [%.6f, %.6f]\n', ...
    thetaNormalized(1), thetaNormalized(2));
if rawDiverged
    fprintf('Raw estimator exceeded %.1e at sample %d.\n', ...
        divergenceLimit, rawDivergedAt);
else
    fprintf('Final raw estimate: [%.6f, %.6f]\n', thetaRaw(1), thetaRaw(2));
end
fprintf('Saved Chapter9_Lesson4_results_matlab.csv.\n');

%% Simulink use
% In a MATLAB Function block, declare thetaHat as a discrete state outside
% this function (for example, with a Unit Delay block), then call:
%
% thetaNext = normalizedUpdateStep(thetaHat, phi, predictionError, mu, delta)
%
% Feed thetaNext back through the Unit Delay. Compute predictionError as
% measuredOutput - thetaHat.' * phi. The denominator block must implement
% delta + phi.' * phi, with delta strictly positive.

function thetaNext = normalizedUpdateStep( ...
        thetaHat, phi, predictionError, stepSize, delta)
    denominator = delta + phi.' * phi;
    thetaNext = thetaHat + stepSize * phi * predictionError / denominator;
end

For Simulink, place the parameter estimate in a Unit Delay block. Form \( \hat y_k=\hat\theta_k^T\phi_k \), subtract it from the measured output, calculate \( m_k^2=\delta+\phi_k^T\phi_k \), and call the local normalizedUpdateStep function in a MATLAB Function block. Feed thetaNext back to the Unit Delay input. Explicitly setting the initial condition of the delay defines \( \hat\theta_0 \).

15. Wolfram Mathematica Implementation

Chapter9_Lesson4.nb


Notebook[{
  Cell[
    "Chapter 9, Lesson 4: Normalization of Signals and Regressors",
    "Title"
  ],
  Cell[
    "This notebook compares raw and normalized gradient estimators when the regressor magnitude changes abruptly.",
    "Text"
  ],

  Cell[BoxData["ClearAll[\"Global`*\"]

nSamples = 1600;
trueTheta = {2.0, -1.0};
muRaw = 0.04;
muNormalized = 0.8;
delta = 0.1;
divergenceLimit = 10.^6;

regressor[k_Integer] := Module[{amplitude},
  amplitude = Which[
    k < 500, 1.0,
    k < 850, 10.0,
    True, 1.5
  ];
  {
    Sin[0.037 k] + 0.5 Cos[0.011 k],
    amplitude Cos[0.023 k]
  }
];

normalizedGradientStep[
  theta_,
  phi_,
  predictionError_,
  stepSize_,
  deltaValue_
] := theta +
  stepSize phi predictionError/(deltaValue + phi.phi);"
  ], "Input"],

  Cell[BoxData["thetaRaw = {0.0, 0.0};
thetaNormalized = {0.0, 0.0};
rawDiverged = False;
rawDivergedAt = Missing[\"NotAvailable\"];

rawHistory = ConstantArray[Indeterminate, {nSamples, 2}];
normalizedHistory = ConstantArray[0.0, {nSamples, 2}];
regressorNormHistory = ConstantArray[0.0, nSamples];
normalizerHistory = ConstantArray[0.0, nSamples];

Do[
  phi = regressor[k];
  noise = 0.01 Sin[0.071 k];
  measuredOutput = trueTheta.phi + noise;

  normalizedError =
    measuredOutput - thetaNormalized.phi;

  thetaNormalized = normalizedGradientStep[
    thetaNormalized,
    phi,
    normalizedError,
    muNormalized,
    delta
  ];

  If[! rawDiverged,
    rawError = measuredOutput - thetaRaw.phi;
    thetaRaw = thetaRaw + muRaw phi rawError;

    If[
      ! VectorQ[thetaRaw, NumericQ] ||
      Norm[thetaRaw] > divergenceLimit,
      rawDiverged = True;
      rawDivergedAt = k,
      rawHistory[[k + 1]] = thetaRaw
    ];
  ];

  normalizedHistory[[k + 1]] = thetaNormalized;
  regressorNormHistory[[k + 1]] = Norm[phi];
  normalizerHistory[[k + 1]] = delta + phi.phi;,
  {k, 0, nSamples - 1}
];"
  ], "Input"],

  Cell[BoxData["Print[
  \"True parameter: \",
  trueTheta
];

Print[
  \"Final normalized estimate: \",
  Last[normalizedHistory]
];

If[
  rawDiverged,
  Print[
    \"Raw estimator exceeded \",
    divergenceLimit,
    \" at sample \",
    rawDivergedAt,
    \".\"
  ],
  Print[
    \"Final raw estimate: \",
    thetaRaw
  ]
];

parameterPlot = ListLinePlot[
  {
    normalizedHistory[[All, 1]],
    normalizedHistory[[All, 2]],
    rawHistory[[All, 1]],
    rawHistory[[All, 2]]
  },
  PlotLegends -> {
    \"normalized theta_1\",
    \"normalized theta_2\",
    \"raw theta_1\",
    \"raw theta_2\"
  },
  AxesLabel -> {
    \"sample\",
    \"parameter estimate\"
  },
  PlotRange -> All,
  ImageSize -> Large
];

parameterPlot"
  ], "Input"],

  Cell[BoxData["rows = MapThread[
  Join,
  {
    List /@ Range[0, nSamples - 1],
    List /@ regressorNormHistory,
    List /@ normalizerHistory,
    rawHistory,
    normalizedHistory
  }
];

Export[
  \"Chapter9_Lesson4_results_mathematica.csv\",
  Prepend[
    rows,
    {
      \"sample\",
      \"regressor_norm\",
      \"normalizer\",
      \"raw_theta_1\",
      \"raw_theta_2\",
      \"normalized_theta_1\",
      \"normalized_theta_2\"
    }
  ]
];"
  ], "Input"]
},
  WindowTitle -> "Chapter9_Lesson4",
  Saveable -> True
]        

16. Interpreting the Numerical Results

Before the burst, both estimators receive regressors of modest magnitude. The raw gain is therefore adequate and both estimates move toward the true parameter. During the burst, the raw effective step is governed by \( \mu_{\mathrm{raw}}\|\phi_k\|^2 \). Near the peaks of the second regressor component this quantity exceeds the discrete-time stability interval, and the raw parameter error grows rapidly.

For the normalized law, the corresponding directional factor is

\[ \mu\frac{\|\phi_k\|^2} {\delta+\|\phi_k\|^2}<\mu=0.8<2. \]

Hence the regressor burst does not invalidate the nominal step-size inequality. Small measurement noise causes a nonzero steady parameter fluctuation, and normalization alone does not eliminate this effect. Leakage, dead-zone logic, projection, or reduced adaptation gain may be added when noise and unmodeled dynamics dominate.

17. Design Rules and Common Misconceptions

17.1 Selecting the discrete gain

For the simple Euclidean normalized gradient law, start with \( 0 < \mu\leq 1 \). Values near one produce fast correction in noise-free regression. Values closer to zero reduce noise sensitivity and parameter chatter. The theoretical upper interval \( \mu < 2 \) is not a recommendation to operate near two in noisy closed-loop systems.

17.2 Selecting the regularization constant

The constant \( \delta \) should be small compared with normal regressor energy but large enough to suppress update amplification when signals approach zero. If typical dimensionless regressor energy is near one, values between \( 10^{-3} \) and \( 10^{-1} \) are common starting points, followed by simulation and robustness testing.

17.3 Normalization is not data-set standardization

Statistical standardization subtracts a mean and divides by a standard deviation estimated from a data set. Adaptive-law normalization uses an instantaneous or dynamically filtered positive signal to control update scale online. The two operations have different objectives and proofs.

17.4 Normalization does not create excitation

If the regressor lacks independent directional information, normalization cannot reconstruct it. It can improve numerical behavior while leaving some parameter directions unidentifiable. The formal excitation condition is studied next in Chapter 10.

17.5 Normalization does not impose parameter bounds

A normalized estimate can still drift under disturbances or modeling error. Projection imposes admissible parameter constraints; robust modifications manage persistent perturbations; normalization moderates signal-dependent gain. These mechanisms should not be treated as interchangeable.

18. Problems and Solutions

Problem 1 (Normalized-regressor bound): Let \( m^2=\delta+\phi^T\phi \) with \( \delta > 0 \). Prove that \( \|\phi/m\| < 1 \) and determine its limiting value as \( \|\phi\|\to\infty \).

Solution:

\[ \left\|\frac{\phi}{m}\right\|^2 =\frac{\|\phi\|^2}{\delta+\|\phi\|^2} =1-\frac{\delta}{\delta+\|\phi\|^2}<1. \]

Taking the square root preserves the strict inequality because both sides are nonnegative. As \( \|\phi\|\to\infty \), the fraction tends to one; therefore \( \|\phi/m\|\to 1 \) from below.

Problem 2 (Discrete Lyapunov decrement): For

\[ \hat\theta_{k+1}=\hat\theta_k+ \mu\frac{\phi_k\varepsilon_k}{m_k^2},\qquad m_k^2=\delta+\phi_k^T\phi_k, \]

derive \( V_{k+1}-V_k \) for \( V_k=\|\tilde\theta_k\|^2 \) and prove that \( 0 < \mu < 2 \) is sufficient for nonincrease.

Solution: Since

\[ \tilde\theta_{k+1}=\tilde\theta_k- \mu\frac{\phi_k\varepsilon_k}{m_k^2}, \]

direct expansion yields

\[ \begin{aligned} V_{k+1}-V_k &=-2\mu\frac{\varepsilon_k^2}{m_k^2} +\mu^2\frac{\varepsilon_k^2\|\phi_k\|^2}{m_k^4} \\ &=-\mu\left(2-\mu q_k\right) \frac{\varepsilon_k^2}{m_k^2}, \end{aligned} \]

where \( q_k=\|\phi_k\|^2/m_k^2 < 1 \). For \( 0 < \mu < 2 \), one has \( 2-\mu q_k > 2-\mu > 0 \), hence \( V_{k+1}-V_k\leq 0 \).

Problem 3 (Effect of changing physical units): A sensor conversion changes \( \phi \) and \( y \) to \( c\phi \) and \( cy \). Compare the raw and normalized update directions.

Solution: The raw direction becomes

\[ (c\phi)(c\varepsilon)=c^2\phi\varepsilon, \]

so a unit conversion changes the effective adaptive gain by \( c^2 \). The normalized direction becomes

\[ \frac{c^2\phi\varepsilon} {\delta+c^2\phi^T\phi}. \]

For regressor energy large relative to \( \delta/c^2 \), the factor \( c^2 \) cancels approximately. Thus the normalized law is much less sensitive to physical scaling.

Problem 4 (Dynamic-normalizer positivity): Let \( \dot n=-\lambda_n n+\|\phi\|^2 \), with \( \lambda_n > 0 \) and \( n(0)\geq 0 \). Prove that \( n(t)\geq 0 \) for all \( t\geq 0 \).

Solution: Variation of constants gives

\[ n(t)=e^{-\lambda_n t}n(0)+ \int_0^t e^{-\lambda_n(t-\tau)}\|\phi(\tau)\|^2d\tau. \]

Every factor in both terms is nonnegative, so their sum is nonnegative. Consequently \( m^2=\delta+n\geq\delta > 0 \).

Problem 5 (Normalization plus projection): Assume \( \theta^*\in\Omega \) and the projection property

\[ \tilde\theta^T\Gamma^{-1} \left(\operatorname{Proj}_{\Omega}(\hat\theta,v)-v\right) \geq 0. \]

Show that choosing \( v=\Gamma\phi\varepsilon/m^2 \) preserves the nonpositive Lyapunov derivative.

Solution: With

\[ V=\frac{1}{2}\tilde\theta^T\Gamma^{-1}\tilde\theta, \]

we obtain

\[ \begin{aligned} \dot V &=-\tilde\theta^T\Gamma^{-1} \operatorname{Proj}_{\Omega}(\hat\theta,v) \\ &\leq-\tilde\theta^T\Gamma^{-1}v =-\frac{\tilde\theta^T\phi\varepsilon}{m^2} =-\frac{\varepsilon^2}{m^2}\leq0. \end{aligned} \]

The projection correction can only make the derivative at least as favorable as the unprojected normalized update under the stated sign convention.

Problem 6 (Gain selection from a worst-case raw regressor): Suppose a raw discrete gradient law is required to remain nonexpansive for all \( \|\phi_k\|\leq 12 \). Determine a sufficient upper bound on the scalar raw gain. Compare it with the normalized law.

Solution: The raw sufficient condition is

\[ 0 < \mu_{\mathrm{raw}}\|\phi_k\|^2 < 2. \]

Using the worst case,

\[ \mu_{\mathrm{raw}} < \frac{2}{12^2} =\frac{1}{72}\approx 0.01389. \]

The normalized Euclidean law instead admits the scale-independent sufficient interval \( 0 < \mu < 2 \). In practice, noise and closed-loop interactions motivate a more conservative value.

19. Summary

Normalization divides an adaptive update by a positive measure of regressor energy. For \( m^2=\delta+\phi^T\phi \), the normalized regressor has norm below one, the ideal update loses its quadratic dependence on regressor magnitude, and the discrete normalized gradient law has the scale-independent sufficient gain interval \( 0 < \mu < 2 \). Continuous-time Lyapunov analysis yields \( \dot V=-\varepsilon^2/m^2\leq0 \). Dynamic normalizers can smooth energy estimates, while projection can be combined with normalization to enforce parameter constraints without destroying the Lyapunov inequality. Normalization improves numerical conditioning and robustness to signal scaling, but it does not create excitation, remove noise, or replace projection and robust modifications.

20. References

  1. Monopoli, R.V. (1974). Model reference adaptive control with an augmented error. IEEE Transactions on Automatic Control, 19(5), 474–484.
  2. Anderson, B.D.O. (1985). Adaptive systems, lack of persistency of excitation and bursting phenomena. Automatica, 21(3), 247–258.
  3. Cluett, W.R. (1988). Some comments on the use of normalization in robust adaptive control. Systems & Control Letters, 11(2), 135–138.
  4. Feng, G., & Palaniswami, M. (1994). Robust direct adaptive controllers with a new normalization technique. IEEE Transactions on Automatic Control, 39(11), 2330–2334.
  5. Barabanov, N.E., Ortega, R., & Astolfi, A. (2005). Is normalization necessary for stable model reference adaptive control? IEEE Transactions on Automatic Control, 50(9), 1384–1390.
  6. Gao, Q.-Z., & Xie, X.-J. (2010). Robustness analysis of discrete-time indirect model reference adaptive control with normalized adaptive laws. International Journal of Automation and Computing, 7, 381–388.
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.