Chapter 23: Pole Placement for Single-Input Systems

Lesson 3: Ackermann’s Formula – Concept and Application

This lesson derives Ackermann’s formula for continuous-time SISO pole placement, explains why controllability is the decisive algebraic condition, and shows how the formula converts a desired characteristic polynomial directly into the state-feedback gain \( K \) for \( u=-Kx \).

1. Problem Setting and Lesson Context

In the previous lesson, pole assignment was performed by transforming a controllable single-input system into controllable canonical form. Here we obtain the same feedback gain without explicitly writing the full coordinate transformation. Consider the continuous-time SISO system

\[ \dot{x}=Ax+Bu,\qquad x\in\mathbb{R}^{n},\quad B\in\mathbb{R}^{n\times 1}. \]

With full-state feedback \( u=-Kx \), the closed-loop matrix is

\[ A_{cl}=A-BK. \]

The pole placement objective is to choose \( K=[k_1\;k_2\;\cdots\;k_n] \) such that

\[ \det(sI-A+BK)=p_d(s), \]

where the desired monic characteristic polynomial is

\[ p_d(s)=s^n+\alpha_{n-1}s^{n-1}+\cdots+\alpha_1s+\alpha_0 =\prod_{i=1}^{n}(s-\lambda_i^{d}). \]

Ackermann’s formula is compact, but it is not merely a computational trick. It is a coordinate-free expression of the canonical-form pole assignment law through the controllability matrix.

2. Controllability Matrix and the Key Algebraic Object

For a single-input system, define the controllability matrix

\[ \mathcal{C}=[B\;AB\;A^2B\;\cdots\;A^{n-1}B]. \]

The system is controllable exactly when \( \operatorname{rank}(\mathcal{C})=n \). In that case \( \mathcal{C}^{-1} \) exists, and the last row selector

\[ e_n^T=[0\;0\;\cdots\;0\;1] \]

extracts the coordinate direction corresponding to the highest-order phase variable in controllable canonical coordinates. The desired matrix polynomial is obtained by substituting \( A \) into \( p_d(s) \):

\[ p_d(A)=A^n+\alpha_{n-1}A^{n-1}+\cdots+\alpha_1A+\alpha_0I. \]

The formula is then

\[ K=e_n^T\mathcal{C}^{-1}p_d(A). \]

flowchart TD
  A["Given SISO pair A, B"] --> B["Build controllability matrix C"]
  B --> C["Check rank(C) = n"]
  C -->|"no"| D["Arbitrary pole placement impossible"]
  C -->|"yes"| E["Choose desired poles"]
  E --> F["Build desired polynomial p_d(s)"]
  F --> G["Evaluate matrix polynomial p_d(A)"]
  G --> H["Compute K = e_n^T inv(C) p_d(A)"]
  H --> I["Verify eig(A - B K)"]
        

3. Derivation from Controllable Canonical Form

Let the open-loop characteristic polynomial of \( A \) be

\[ p_A(s)=\det(sI-A)=s^n+a_{n-1}s^{n-1}+\cdots+a_1s+a_0. \]

In controllable canonical form, the system has

\[ A_c=\begin{bmatrix} 0 & 1 & 0 & \cdots & 0\\ 0 & 0 & 1 & \cdots & 0\\ \vdots & \vdots & \vdots & \ddots & \vdots\\ 0 & 0 & 0 & \cdots & 1\\ -a_0 & -a_1 & -a_2 & \cdots & -a_{n-1} \end{bmatrix},\qquad B_c=\begin{bmatrix}0\\0\\\vdots\\0\\1\end{bmatrix}. \]

Applying \( u=-K_cz \), where \( K_c=[\kappa_0\;\kappa_1\;\cdots\;\kappa_{n-1}] \), changes only the last row:

\[ A_c-B_cK_c=\begin{bmatrix} 0 & 1 & 0 & \cdots & 0\\ 0 & 0 & 1 & \cdots & 0\\ \vdots & \vdots & \vdots & \ddots & \vdots\\ 0 & 0 & 0 & \cdots & 1\\ -(a_0+\kappa_0) & -(a_1+\kappa_1) & \cdots & -(a_{n-2}+\kappa_{n-2}) & -(a_{n-1}+\kappa_{n-1}) \end{bmatrix}. \]

Therefore the closed-loop characteristic polynomial is

\[ s^n+(a_{n-1}+\kappa_{n-1})s^{n-1}+\cdots+(a_1+\kappa_1)s+(a_0+\kappa_0). \]

Matching it with \( p_d(s) \) gives

\[ \begin{aligned} \kappa_i &= \alpha_i-a_i, \qquad i=0,1,\ldots,n-1. \end{aligned} \]

Ackermann’s formula packages this canonical-coordinate gain back into the original coordinates. If \( x=Tz \), then \( K=K_cT^{-1} \). The product \( e_n^T\mathcal{C}^{-1}p_d(A) \) is exactly that coordinate pullback.

4. Formal Statement and Proof

Theorem: Let \( (A,B) \) be a controllable SISO pair with \( A\in\mathbb{R}^{n\times n} \) and \( B\in\mathbb{R}^{n\times 1} \). For any monic polynomial \( p_d(s)=s^n+\alpha_{n-1}s^{n-1}+\cdots+\alpha_0 \), the feedback

\[ K=e_n^T\mathcal{C}^{-1}p_d(A) \]

satisfies \( \det(sI-(A-BK))=p_d(s) \).

Proof idea: Because \( (A,B) \) is controllable, there exists a nonsingular transformation \( T \) that maps the system to controllable canonical form. In canonical coordinates, coefficient matching uniquely gives the gain \( K_c \). Since closed-loop eigenvalues are invariant under similarity transformations,

\[ A-BK=T(A_c-B_cK_c)T^{-1}. \]

Hence \( A-BK \) and \( A_c-B_cK_c \) have the same characteristic polynomial. The algebraic identity

\[ K_cT^{-1}=e_n^T\mathcal{C}^{-1}p_d(A) \]

follows from the construction of the controllability transformation and the Cayley-Hamilton relation. Thus Ackermann’s expression produces the original-coordinate feedback that realizes \( p_d(s) \).

The proof also shows why the formula fails when \( \operatorname{rank}(\mathcal{C})<n \): the inverse \( \mathcal{C}^{-1} \) does not exist, and some modes are not affected by the input channel.

5. Worked Numerical Example

Consider

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

The controllability matrix is

\[ \mathcal{C}=[B\;AB] =\begin{bmatrix}0&1\\1&-3\end{bmatrix},\qquad \det(\mathcal{C})=-1\neq0. \]

Choose desired poles \( -2\pm2j \), giving

\[ p_d(s)=(s+2-2j)(s+2+2j)=s^2+4s+8. \]

Hence

\[ p_d(A)=A^2+4A+8I. \]

Ackermann’s formula gives

\[ K=[0\;1]\mathcal{C}^{-1}(A^2+4A+8I)=[6\;1]. \]

Therefore

\[ A-BK=\begin{bmatrix}0&1\\-8&-4\end{bmatrix},\qquad \det(sI-A+BK)=s^2+4s+8. \]

flowchart TD
  P["Desired poles: -2 plus/minus 2j"] --> Q["p_d(s) = s^2 + 4s + 8"]
  Q --> R["p_d(A) = A^2 + 4A + 8I"]
  R --> S["K = e2^T inv(C) p_d(A)"]
  S --> T["K = [6, 1]"]
  T --> U["eig(A - B K) = desired poles"]
        

6. Algorithmic Procedure and Numerical Warnings

The direct algorithm is:

  1. Build \( \mathcal{C}=[B\;AB\;\cdots\;A^{n-1}B] \).
  2. Check \( \operatorname{rank}(\mathcal{C})=n \).
  3. Compute \( p_d(s) \) from the desired poles.
  4. Evaluate \( p_d(A) \).
  5. Compute \( K=e_n^T\mathcal{C}^{-1}p_d(A) \).
  6. Verify \( \operatorname{eig}(A-BK) \).

Although Ackermann’s formula is exact in symbolic algebra, it can be numerically fragile. The inverse of \( \mathcal{C} \) may amplify roundoff when \( \kappa(\mathcal{C}) \) is large:

\[ \frac{\|\Delta K\|}{\|K\|}\lesssim \kappa(\mathcal{C})\left(\frac{\|\Delta\mathcal{C}\|}{\|\mathcal{C}\|} +\frac{\|\Delta p_d(A)\|}{\|p_d(A)\|}\right). \]

For high-order systems or poorly scaled states, robust algorithms such as Schur-based pole placement are usually preferred. Ackermann’s formula remains essential because it exposes the exact algebraic structure of SISO pole assignment.

7. Python Implementation

File: Chapter23_Lesson3.py

# Chapter23_Lesson3.py
# Ackermann's formula for SISO pole placement: u = -K x
# Requires: numpy. Optional comparison requires scipy.

import numpy as np


def controllability_matrix(A: np.ndarray, B: np.ndarray) -> np.ndarray:
    """Return C = [B, AB, ..., A^(n-1)B] for a single-input system."""
    A = np.asarray(A, dtype=complex)
    B = np.asarray(B, dtype=complex)
    n = A.shape[0]
    blocks = [B]
    for k in range(1, n):
        blocks.append(np.linalg.matrix_power(A, k) @ B)
    return np.hstack(blocks)


def matrix_polynomial(A: np.ndarray, coefficients: np.ndarray) -> np.ndarray:
    """Evaluate monic polynomial p(A).

    coefficients are [1, alpha_{n-1}, ..., alpha_0], as returned by np.poly.
    p(A) = A^n + alpha_{n-1} A^(n-1) + ... + alpha_0 I.
    """
    A = np.asarray(A, dtype=complex)
    n = A.shape[0]
    result = np.linalg.matrix_power(A, n)
    for i, alpha in enumerate(coefficients[1:]):
        power = n - 1 - i
        term = np.eye(n, dtype=complex) if power == 0 else np.linalg.matrix_power(A, power)
        result = result + alpha * term
    return result


def ackermann_gain(A: np.ndarray, B: np.ndarray, desired_poles) -> np.ndarray:
    """Compute K from Ackermann's formula.

    For x_dot = A x + B u and u = -K x, the closed-loop matrix is A - B K.
    """
    A = np.asarray(A, dtype=complex)
    B = np.asarray(B, dtype=complex).reshape((-1, 1))
    n = A.shape[0]
    if A.shape != (n, n) or B.shape != (n, 1):
        raise ValueError("A must be n by n and B must be n by 1.")

    Ctrb = controllability_matrix(A, B)
    rank = np.linalg.matrix_rank(Ctrb)
    if rank != n:
        raise ValueError(f"System is not controllable: rank(C)={rank}, n={n}.")

    p = np.poly(desired_poles)  # [1, alpha_{n-1}, ..., alpha_0]
    phi_A = matrix_polynomial(A, p)
    e_n_T = np.zeros((1, n), dtype=complex)
    e_n_T[0, -1] = 1.0
    K = e_n_T @ np.linalg.inv(Ctrb) @ phi_A
    return np.real_if_close(K)


def verify(A: np.ndarray, B: np.ndarray, K: np.ndarray):
    Acl = A - B @ K
    return np.linalg.eigvals(Acl), np.poly(Acl)


if __name__ == "__main__":
    A = np.array([[0.0, 1.0],
                  [-2.0, -3.0]])
    B = np.array([[0.0],
                  [1.0]])
    desired = [-2 + 2j, -2 - 2j]

    K = ackermann_gain(A, B, desired)
    poles, char_poly = verify(A, B, K)

    print("K =", K)
    print("closed-loop poles =", poles)
    print("closed-loop characteristic coefficients =", char_poly)

    # Optional comparison with SciPy:
    try:
        from scipy.signal import place_poles
        K_scipy = place_poles(A, B, desired).gain_matrix
        print("SciPy place_poles K =", K_scipy)
    except Exception as exc:
        print("SciPy comparison skipped:", exc)

8. C++ Implementation

File: Chapter23_Lesson3.cpp

// Chapter23_Lesson3.cpp
// Ackermann's formula for SISO pole placement: u = -Kx
// Compile: g++ -std=c++17 Chapter23_Lesson3.cpp -O2 -o Chapter23_Lesson3

#include <cmath>
#include <complex>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <vector>

using C = std::complex<double>;
using Matrix = std::vector<std::vector<C>>;

Matrix zeros(int r, int c) {
    return Matrix(r, std::vector<C>(c, C(0.0, 0.0)));
}

Matrix eye(int n) {
    Matrix I = zeros(n, n);
    for (int i = 0; i < n; ++i) I[i][i] = 1.0;
    return I;
}

Matrix add(const Matrix& A, const Matrix& B) {
    int r = (int)A.size(), c = (int)A[0].size();
    Matrix R = zeros(r, c);
    for (int i = 0; i < r; ++i)
        for (int j = 0; j < c; ++j)
            R[i][j] = A[i][j] + B[i][j];
    return R;
}

Matrix scale(C a, const Matrix& A) {
    int r = (int)A.size(), c = (int)A[0].size();
    Matrix R = zeros(r, c);
    for (int i = 0; i < r; ++i)
        for (int j = 0; j < c; ++j)
            R[i][j] = a * A[i][j];
    return R;
}

Matrix mul(const Matrix& A, const Matrix& B) {
    int r = (int)A.size(), m = (int)A[0].size(), c = (int)B[0].size();
    Matrix R = zeros(r, c);
    for (int i = 0; i < r; ++i)
        for (int k = 0; k < m; ++k)
            for (int j = 0; j < c; ++j)
                R[i][j] += A[i][k] * B[k][j];
    return R;
}

Matrix mpow(Matrix A, int p) {
    int n = (int)A.size();
    Matrix R = eye(n);
    while (p > 0) {
        if (p & 1) R = mul(R, A);
        A = mul(A, A);
        p >>= 1;
    }
    return R;
}

Matrix inverse(Matrix A) {
    int n = (int)A.size();
    Matrix Aug = zeros(n, 2 * n);
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) Aug[i][j] = A[i][j];
        Aug[i][n + i] = 1.0;
    }
    for (int col = 0; col < n; ++col) {
        int pivot = col;
        for (int r = col + 1; r < n; ++r) {
            if (std::abs(Aug[r][col]) > std::abs(Aug[pivot][col])) pivot = r;
        }
        if (std::abs(Aug[pivot][col]) < 1e-12) {
            throw std::runtime_error("Matrix is singular or ill-conditioned.");
        }
        std::swap(Aug[pivot], Aug[col]);
        C div = Aug[col][col];
        for (int j = 0; j < 2 * n; ++j) Aug[col][j] /= div;
        for (int r = 0; r < n; ++r) {
            if (r == col) continue;
            C factor = Aug[r][col];
            for (int j = 0; j < 2 * n; ++j) Aug[r][j] -= factor * Aug[col][j];
        }
    }
    Matrix Inv = zeros(n, n);
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
            Inv[i][j] = Aug[i][n + j];
    return Inv;
}

Matrix controllability(const Matrix& A, const Matrix& B) {
    int n = (int)A.size();
    Matrix Ctrb = zeros(n, n);
    Matrix block = B;
    for (int k = 0; k < n; ++k) {
        for (int i = 0; i < n; ++i) Ctrb[i][k] = block[i][0];
        block = mul(A, block);
    }
    return Ctrb;
}

std::vector<C> polynomialFromRoots(const std::vector<C>& roots) {
    std::vector<C> coeff = {1.0};
    for (C r : roots) {
        std::vector<C> next(coeff.size() + 1, 0.0);
        for (size_t i = 0; i < coeff.size(); ++i) {
            next[i] += coeff[i];
            next[i + 1] += -r * coeff[i];
        }
        coeff = next;
    }
    return coeff; // [1, alpha_{n-1}, ..., alpha_0]
}

Matrix matrixPolynomial(const Matrix& A, const std::vector<C>& coeff) {
    int n = (int)A.size();
    Matrix R = mpow(A, n);
    for (int i = 1; i <= n; ++i) {
        int power = n - i;
        Matrix term = (power == 0) ? eye(n) : mpow(A, power);
        R = add(R, scale(coeff[i], term));
    }
    return R;
}

Matrix ackermannGain(const Matrix& A, const Matrix& B, const std::vector<C>& desiredPoles) {
    int n = (int)A.size();
    Matrix Ctrb = controllability(A, B);
    Matrix CtrbInv = inverse(Ctrb);
    std::vector<C> p = polynomialFromRoots(desiredPoles);
    Matrix phiA = matrixPolynomial(A, p);
    Matrix eT = zeros(1, n);
    eT[0][n - 1] = 1.0;
    return mul(mul(eT, CtrbInv), phiA);
}

int main() {
    Matrix A = {{0.0, 1.0}, {-2.0, -3.0}};
    Matrix B = {{0.0}, {1.0}};
    std::vector<C> desired = {C(-2.0, 2.0), C(-2.0, -2.0)};

    Matrix K = ackermannGain(A, B, desired);
    std::cout << std::fixed << std::setprecision(6);
    std::cout << "K = [ ";
    for (auto v : K[0]) std::cout << v.real() << " ";
    std::cout << "]\n";
    std::cout << "Expected for this example: K = [ 6 1 ]\n";
    return 0;
}

9. Java Implementation

File: Chapter23_Lesson3.java

// Chapter23_Lesson3.java
// Ackermann's formula for SISO pole placement using basic matrix operations.
// Compile: javac Chapter23_Lesson3.java
// Run:     java Chapter23_Lesson3

import java.util.Arrays;

public class Chapter23_Lesson3 {
    static double[][] zeros(int r, int c) {
        return new double[r][c];
    }

    static double[][] eye(int n) {
        double[][] I = zeros(n, n);
        for (int i = 0; i < n; i++) I[i][i] = 1.0;
        return I;
    }

    static double[][] add(double[][] A, double[][] B) {
        int r = A.length, c = A[0].length;
        double[][] R = zeros(r, c);
        for (int i = 0; i < r; i++)
            for (int j = 0; j < c; j++)
                R[i][j] = A[i][j] + B[i][j];
        return R;
    }

    static double[][] scale(double a, double[][] A) {
        int r = A.length, c = A[0].length;
        double[][] R = zeros(r, c);
        for (int i = 0; i < r; i++)
            for (int j = 0; j < c; j++)
                R[i][j] = a * A[i][j];
        return R;
    }

    static double[][] mul(double[][] A, double[][] B) {
        int r = A.length, m = A[0].length, c = B[0].length;
        double[][] R = zeros(r, c);
        for (int i = 0; i < r; i++)
            for (int k = 0; k < m; k++)
                for (int j = 0; j < c; j++)
                    R[i][j] += A[i][k] * B[k][j];
        return R;
    }

    static double[][] mpow(double[][] A, int p) {
        int n = A.length;
        double[][] R = eye(n);
        double[][] X = A;
        while (p > 0) {
            if ((p & 1) == 1) R = mul(R, X);
            X = mul(X, X);
            p >>= 1;
        }
        return R;
    }

    static double[][] inverse(double[][] A) {
        int n = A.length;
        double[][] aug = zeros(n, 2 * n);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) aug[i][j] = A[i][j];
            aug[i][n + i] = 1.0;
        }
        for (int col = 0; col < n; col++) {
            int pivot = col;
            for (int r = col + 1; r < n; r++)
                if (Math.abs(aug[r][col]) > Math.abs(aug[pivot][col])) pivot = r;
            if (Math.abs(aug[pivot][col]) < 1e-12)
                throw new RuntimeException("Matrix is singular or ill-conditioned.");
            double[] temp = aug[pivot]; aug[pivot] = aug[col]; aug[col] = temp;
            double div = aug[col][col];
            for (int j = 0; j < 2 * n; j++) aug[col][j] /= div;
            for (int r = 0; r < n; r++) {
                if (r == col) continue;
                double factor = aug[r][col];
                for (int j = 0; j < 2 * n; j++) aug[r][j] -= factor * aug[col][j];
            }
        }
        double[][] inv = zeros(n, n);
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                inv[i][j] = aug[i][n + j];
        return inv;
    }

    static double[][] controllability(double[][] A, double[][] B) {
        int n = A.length;
        double[][] C = zeros(n, n);
        double[][] block = B;
        for (int k = 0; k < n; k++) {
            for (int i = 0; i < n; i++) C[i][k] = block[i][0];
            block = mul(A, block);
        }
        return C;
    }

    static double[][] matrixPolynomial(double[][] A, double[] coeff) {
        int n = A.length;
        double[][] R = mpow(A, n);
        for (int i = 1; i <= n; i++) {
            int power = n - i;
            double[][] term = (power == 0) ? eye(n) : mpow(A, power);
            R = add(R, scale(coeff[i], term));
        }
        return R;
    }

    static double[] ackermannGain(double[][] A, double[][] B, double[] desiredPoly) {
        int n = A.length;
        double[][] C = controllability(A, B);
        double[][] Cinv = inverse(C);
        double[][] phiA = matrixPolynomial(A, desiredPoly);
        double[][] eT = zeros(1, n);
        eT[0][n - 1] = 1.0;
        double[][] K = mul(mul(eT, Cinv), phiA);
        return K[0];
    }

    public static void main(String[] args) {
        double[][] A = {{0.0, 1.0}, {-2.0, -3.0}};
        double[][] B = {{0.0}, {1.0}};

        // Desired poles -2 +/- 2i give p_d(s) = s^2 + 4s + 8.
        double[] desiredPoly = {1.0, 4.0, 8.0};
        double[] K = ackermannGain(A, B, desiredPoly);
        System.out.println("K = " + Arrays.toString(K));
        System.out.println("Expected for this example: K = [6.0, 1.0]");
    }
}

10. MATLAB and Simulink Implementation

File: Chapter23_Lesson3.m

% Chapter23_Lesson3.m
% Ackermann's formula for SISO pole placement.
% The Control System Toolbox has acker(A,B,poles), but this script also
% implements the formula explicitly.

clear; clc;

A = [0 1; -2 -3];
B = [0; 1];
desired_poles = [-2+2i, -2-2i];

K = ackermann_gain(A, B, desired_poles);
disp('K from custom Ackermann formula:');
disp(K);

Acl = A - B*K;
disp('Closed-loop eigenvalues:');
disp(eig(Acl));

if exist('acker', 'file') == 2
    disp('K from MATLAB acker:');
    disp(acker(A, B, desired_poles));
end

% Simulink note:
% Use a State-Space block with A, B, C=eye(size(A)), D=zeros(size(B)).
% Feed the measured state vector x into a Gain block with gain -K, then feed
% that signal into the State-Space block input. Scope x and u to verify decay.

function Ctrb = controllability_matrix(A, B)
    n = size(A, 1);
    Ctrb = zeros(n, n);
    for k = 1:n
        Ctrb(:, k) = A^(k-1) * B;
    end
end

function phiA = matrix_polynomial(A, coeff)
    % coeff = [1 alpha_{n-1} ... alpha_0]
    n = size(A, 1);
    phiA = A^n;
    for i = 2:length(coeff)
        power = n - (i - 1);
        if power == 0
            term = eye(n);
        else
            term = A^power;
        end
        phiA = phiA + coeff(i) * term;
    end
end

function K = ackermann_gain(A, B, desired_poles)
    n = size(A, 1);
    Ctrb = controllability_matrix(A, B);
    if rank(Ctrb) ~= n
        error('System is not controllable, so arbitrary pole placement is impossible.');
    end
    coeff = poly(desired_poles);
    phiA = matrix_polynomial(A, coeff);
    eT = zeros(1, n);
    eT(end) = 1;
    K = eT / Ctrb * phiA;
    K = real(K);
end

For Simulink, use a State-Space block for \( \dot{x}=Ax+Bu \), connect the output state vector to a Gain block with gain \( -K \), and feed the result back to the input. Scopes on \( x(t) \) and \( u(t) \) verify the closed-loop decay and control magnitude.

11. Wolfram Mathematica Implementation

File: Chapter23_Lesson3.nb

Notebook[{
  Cell["Chapter23_Lesson3.nb", "Title"],
  Cell["Ackermann's formula for SISO pole placement", "Subtitle"],
  Cell[BoxData[ToBoxes[
    ClearAll[controllabilityMatrix, matrixPolynomial, ackermannGain];
    controllabilityMatrix[A_, B_] := Module[{n = Length[A]},
      Transpose[Table[Flatten[MatrixPower[A, k].B], {k, 0, n - 1}]]
    ];
    matrixPolynomial[A_, coeff_] := Module[{n = Length[A]},
      MatrixPower[A, n] + Sum[coeff[[i + 1]] If[n - i == 0, IdentityMatrix[n], MatrixPower[A, n - i]], {i, 1, n}]
    ];
    ackermannGain[A_, B_, desiredPoles_] := Module[{n, Ctrb, coeff, phiA, eT},
      n = Length[A];
      Ctrb = controllabilityMatrix[A, B];
      If[MatrixRank[Ctrb] != n, Print["System is not controllable."]; Abort[]];
      coeff = CoefficientList[Expand[Times @@ (s - # & /@ desiredPoles)], s] // Reverse;
      phiA = matrixPolynomial[A, coeff];
      eT = UnitVector[n, n];
      Chop[eT.Inverse[Ctrb].phiA]
    ];
  ]], "Input"],
  Cell[BoxData[ToBoxes[
    A = {{0, 1}, {-2, -3}};
    B = {{0}, {1}};
    desiredPoles = {-2 + 2 I, -2 - 2 I};
    K = ackermannGain[A, B, desiredPoles]
  ]], "Input"],
  Cell[BoxData[ToBoxes[
    Eigenvalues[A - B.K]
  ]], "Input"]
}]

12. Problems and Solutions

Problem 1: For \( A=\begin{bmatrix}0&1\\-5&-2\end{bmatrix} \), \( B=\begin{bmatrix}0\\1\end{bmatrix} \), place the closed-loop poles at \( -3 \) and \( -4 \).

Solution: The desired polynomial is

\[ p_d(s)=(s+3)(s+4)=s^2+7s+12. \]

With \( K=[k_1\;k_2] \),

\[ A-BK=\begin{bmatrix}0&1\\-(5+k_1)&-(2+k_2)\end{bmatrix}. \]

Thus the closed-loop polynomial is \( s^2+(2+k_2)s+(5+k_1) \). Coefficient matching gives

\[ 2+k_2=7,\qquad 5+k_1=12,\qquad K=[7\;5]. \]

Problem 2: Show that arbitrary pole placement is impossible for \( A=\begin{bmatrix}1&0\\0&2\end{bmatrix} \), \( B=\begin{bmatrix}1\\0\end{bmatrix} \).

Solution:

\[ \mathcal{C}=[B\;AB] =\begin{bmatrix}1&1\\0&0\end{bmatrix},\qquad \operatorname{rank}(\mathcal{C})=1<2. \]

The second state is not influenced by the input. Therefore the pole at \( 2 \) is uncontrollable and cannot be moved by any state-feedback gain.

Problem 3: Prove that for a controllable second-order pair in companion form, Ackermann’s formula gives the same gain as coefficient matching.

Solution: Let

\[ A=\begin{bmatrix}0&1\\-a_0&-a_1\end{bmatrix},\qquad B=\begin{bmatrix}0\\1\end{bmatrix},\qquad p_d(s)=s^2+\alpha_1s+\alpha_0. \]

Coefficient matching gives \( K=[\alpha_0-a_0\;\alpha_1-a_1] \). Since

\[ \mathcal{C}=\begin{bmatrix}0&1\\1&-a_1\end{bmatrix},\qquad p_d(A)=A^2+\alpha_1A+ \alpha_0I, \]

direct multiplication of \( [0\;1]\mathcal{C}^{-1}p_d(A) \) yields exactly \( [\alpha_0-a_0\;\alpha_1-a_1] \).

Problem 4: Why should one avoid explicitly computing \( \mathcal{C}^{-1} \) for a high-order system?

Solution: The columns \( B,AB,\ldots,A^{n-1}B \) can become nearly linearly dependent, especially when \( A \) has widely separated modal scales. Then \( \mathcal{C} \) is ill-conditioned, and the gain becomes sensitive to roundoff. Numerically, solving a linear system or using Schur-based pole placement is better than forming the inverse explicitly.

13. Summary

Ackermann’s formula gives a direct SISO state-feedback gain \( K=e_n^T\mathcal{C}^{-1}p_d(A) \) for assigning the closed-loop poles of \( A-BK \). Its existence requires controllability, its derivation follows from controllable canonical form and coefficient matching, and its numerical use requires caution when the controllability matrix is poorly conditioned.

14. References

  1. Kalman, R.E. (1960). On the general theory of control systems. Proceedings of the First International Congress on Automatic Control, 481–492.
  2. Kalman, R.E. (1963). Mathematical description of linear dynamical systems. Journal of the Society for Industrial and Applied Mathematics, Series A: Control, 1(2), 152–192.
  3. Bass, R.W., & Gura, I. (1965). High-order system design via state-space considerations. Proceedings of the Joint Automatic Control Conference.
  4. Wonham, W.M. (1967). On pole assignment in multi-input controllable linear systems. IEEE Transactions on Automatic Control, 12(6), 660–665.
  5. Ackermann, J. (1972). Der Entwurf linearer Regelungssysteme im Zustandsraum. Regelungstechnik und Prozessdatenverarbeitung, 20, 297–300.
  6. Heymann, M. (1968). Comments on pole assignment in multi-input controllable linear systems. IEEE Transactions on Automatic Control, 13(6), 748–749.
  7. Kautsky, J., Nichols, N.K., & Van Dooren, P. (1985). Robust pole assignment in linear state feedback. International Journal of Control, 41(5), 1129–1155.
  8. Tits, A.L., & Yang, Y. (1996). Globally convergent algorithms for robust pole assignment by state feedback. IEEE Transactions on Automatic Control, 41(10), 1432–1452.