Chapter 9: Linearization and Local Behavior
Lesson 4: Linearization of Multi-Input–Multi-Output Nonlinear Systems
This lesson generalizes Jacobian-based linearization to nonlinear systems with multiple inputs and multiple outputs. We derive the full MIMO small-signal state-space model from multivariable Taylor expansions, clarify the role of the four Jacobian blocks \( \mathbf{A}, \mathbf{B}, \mathbf{C}, \mathbf{D} \), and connect the result to a transfer-matrix description \( \mathbf{G}(s) \). We emphasize rigorous remainder bounds, coupling interpretation, and practical computation (symbolic and numerical) in Python, C++, Java, MATLAB/Simulink, and Wolfram Mathematica.
1. Conceptual Overview
In previous lessons, you linearized scalar or single-input models using a Jacobian about an operating point. In control engineering, realistic plants are often coupled: several actuators influence several measured outputs. MIMO linearization produces an LTI approximation suitable for analysis and controller design near an operating point.
The core idea is unchanged: introduce perturbations \( \tilde{\mathbf{x}}(t) \): deviation from an operating state, \( \tilde{\mathbf{u}}(t) \): deviation from an operating input, and \( \tilde{\mathbf{y}}(t) \): deviation from an operating output, then retain only first-order terms.
flowchart TD
A["Nonlinear model: xdot=f(x,u), y=h(x,u)"] --> B["Choose operating point (x0,u0)"]
B --> C["Define perturbations: x_tilde=x-x0, u_tilde=u-u0, y_tilde=y-y0"]
C --> D["Compute Jacobians at (x0,u0): A,B,C,D"]
D --> E["Linear model: x_tilde_dot = A x_tilde + B u_tilde"]
E --> F["Output: y_tilde = C x_tilde + D u_tilde"]
F --> G["Local validation by simulation"]
The distinctive MIMO aspect is that \( \mathbf{A}, \mathbf{B}, \mathbf{C}, \mathbf{D} \) are matrices, and their off-diagonal structure reveals cross-coupling between channels.
2. MIMO Nonlinear Model and Operating Point
Consider a smooth nonlinear state-space model with \( \mathbf{x}(t)\in\mathbb{R}^n \) (states), \( \mathbf{u}(t)\in\mathbb{R}^m \) (inputs), \( \mathbf{y}(t)\in\mathbb{R}^p \) (outputs):
\[ \dot{\mathbf{x}} = \mathbf{f}(\mathbf{x},\mathbf{u}), \qquad \mathbf{y} = \mathbf{h}(\mathbf{x},\mathbf{u}), \qquad \mathbf{f}:\mathbb{R}^n\times\mathbb{R}^m → \mathbb{R}^n,\; \mathbf{h}:\mathbb{R}^n\times\mathbb{R}^m → \mathbb{R}^p. \]
An operating point is a constant pair \( (\mathbf{x}_0,\mathbf{u}_0) \):. If it additionally satisfies \( \mathbf{f}(\mathbf{x}_0,\mathbf{u}_0)=\mathbf{0} \):, it is an equilibrium (steady state).
Define perturbation variables (also called small-signal variables): \( \tilde{\mathbf{x}} \): \( \mathbf{x}-\mathbf{x}_0 \), \( \tilde{\mathbf{u}} \): \( \mathbf{u}-\mathbf{u}_0 \), \( \tilde{\mathbf{y}} \): \( \mathbf{y}-\mathbf{y}_0 \), where \( \mathbf{y}_0=\mathbf{h}(\mathbf{x}_0,\mathbf{u}_0) \):.
\[ \mathbf{x}=\mathbf{x}_0+\tilde{\mathbf{x}},\qquad \mathbf{u}=\mathbf{u}_0+\tilde{\mathbf{u}},\qquad \mathbf{y}=\mathbf{y}_0+\tilde{\mathbf{y}}. \]
3. Multivariable Taylor Expansion and a Remainder Bound
Linearization is the first-order truncation of a multivariable Taylor expansion. It is helpful to combine state and input into a single vector \( \mathbf{z} \): where \( \mathbf{z}=\begin{bmatrix}\mathbf{x}\\\mathbf{u}\end{bmatrix}\in\mathbb{R}^{n+m} \): and similarly define \( \tilde{\mathbf{z}}=\begin{bmatrix}\tilde{\mathbf{x}}\\\tilde{\mathbf{u}}\end{bmatrix} \):.
Theorem (First-order expansion with integral remainder). Let \( \mathbf{F}:\mathbb{R}^{n+m} → \mathbb{R}^q \) be continuously differentiable in a neighborhood of \( \mathbf{z}_0 \). Then for any small \( \tilde{\mathbf{z}} \) in that neighborhood:
\[ \mathbf{F}(\mathbf{z}_0+\tilde{\mathbf{z}}) = \mathbf{F}(\mathbf{z}_0) + \mathbf{J}_F(\mathbf{z}_0)\,\tilde{\mathbf{z}} + \mathbf{r}_F(\tilde{\mathbf{z}}), \]
where \( \mathbf{J}_F(\mathbf{z}_0) \): is the Jacobian at \( \mathbf{z}_0 \), and the remainder is
\[ \mathbf{r}_F(\tilde{\mathbf{z}}) = \int_0^1 \left(\mathbf{J}_F(\mathbf{z}_0+s\tilde{\mathbf{z}})-\mathbf{J}_F(\mathbf{z}_0)\right) \tilde{\mathbf{z}}\; ds. \]
Proof. Define \( \phi(s)=\mathbf{F}(\mathbf{z}_0+s\tilde{\mathbf{z}}) \). By the chain rule, \( \phi'(s)=\mathbf{J}_F(\mathbf{z}_0+s\tilde{\mathbf{z}})\tilde{\mathbf{z}} \). Then
\[ \mathbf{F}(\mathbf{z}_0+\tilde{\mathbf{z}})-\mathbf{F}(\mathbf{z}_0) = \phi(1)-\phi(0) = \int_0^1 \phi'(s)\,ds = \int_0^1 \mathbf{J}_F(\mathbf{z}_0+s\tilde{\mathbf{z}})\tilde{\mathbf{z}}\,ds. \]
Add and subtract \( \mathbf{J}_F(\mathbf{z}_0)\tilde{\mathbf{z}} \) inside the integral to obtain the stated form. □
Corollary (Quadratic error order). If \( \mathbf{J}_F \) is locally Lipschitz near \( \mathbf{z}_0 \), i.e., there exists \( L > 0 \) such that
\[ \|\mathbf{J}_F(\mathbf{z}_a)-\mathbf{J}_F(\mathbf{z}_b)\| \le L\|\mathbf{z}_a-\mathbf{z}_b\| \quad \text{for all }\mathbf{z}_a,\mathbf{z}_b \text{ near }\mathbf{z}_0, \]
then the remainder satisfies \( \|\mathbf{r}_F(\tilde{\mathbf{z}})\| \le \tfrac{L}{2}\|\tilde{\mathbf{z}}\|^2 \):.
Proof. Using the remainder formula and Lipschitz continuity:
\[ \|\mathbf{r}_F(\tilde{\mathbf{z}})\| \le \int_0^1 \|\mathbf{J}_F(\mathbf{z}_0+s\tilde{\mathbf{z}})-\mathbf{J}_F(\mathbf{z}_0)\|\;\|\tilde{\mathbf{z}}\|\; ds \le \int_0^1 (Ls\|\tilde{\mathbf{z}}\|)\;\|\tilde{\mathbf{z}}\|\; ds = \frac{L}{2}\|\tilde{\mathbf{z}}\|^2. \]
□
4. Derivation of the Linearized MIMO Model
Apply Section 3 to both \( \mathbf{f} \) and \( \mathbf{h} \). Writing them as functions of \( \mathbf{z}=[\mathbf{x}^\top\; \mathbf{u}^\top]^\top \), their Jacobians partition into blocks:
\[ \mathbf{J}_f(\mathbf{z}_0) = \begin{bmatrix} \dfrac{\partial \mathbf{f}}{\partial \mathbf{x}} & \dfrac{\partial \mathbf{f}}{\partial \mathbf{u}} \end{bmatrix}_{(\mathbf{x}_0,\mathbf{u}_0)} \triangleq \begin{bmatrix}\mathbf{A} & \mathbf{B}\end{bmatrix}, \qquad \mathbf{J}_h(\mathbf{z}_0) = \begin{bmatrix} \dfrac{\partial \mathbf{h}}{\partial \mathbf{x}} & \dfrac{\partial \mathbf{h}}{\partial \mathbf{u}} \end{bmatrix}_{(\mathbf{x}_0,\mathbf{u}_0)} \triangleq \begin{bmatrix}\mathbf{C} & \mathbf{D}\end{bmatrix}. \]
Using \( \mathbf{x}=\mathbf{x}_0+\tilde{\mathbf{x}} \) and \( \mathbf{u}=\mathbf{u}_0+\tilde{\mathbf{u}} \), we obtain:
\[ \dot{\mathbf{x}} = \mathbf{f}(\mathbf{x}_0,\mathbf{u}_0) + \mathbf{A}\tilde{\mathbf{x}}+\mathbf{B}\tilde{\mathbf{u}} + \mathbf{r}_f(\tilde{\mathbf{x}},\tilde{\mathbf{u}}), \]
\[ \mathbf{y} = \mathbf{h}(\mathbf{x}_0,\mathbf{u}_0) + \mathbf{C}\tilde{\mathbf{x}}+\mathbf{D}\tilde{\mathbf{u}} + \mathbf{r}_h(\tilde{\mathbf{x}},\tilde{\mathbf{u}}). \]
Because \( \dot{\mathbf{x}}=\dot{\tilde{\mathbf{x}}} \) (the operating point is constant), and \( \mathbf{y}_0=\mathbf{h}(\mathbf{x}_0,\mathbf{u}_0) \), this becomes
\[ \dot{\tilde{\mathbf{x}}} = \mathbf{f}(\mathbf{x}_0,\mathbf{u}_0) + \mathbf{A}\tilde{\mathbf{x}}+\mathbf{B}\tilde{\mathbf{u}} + \mathbf{r}_f(\tilde{\mathbf{x}},\tilde{\mathbf{u}}), \qquad \tilde{\mathbf{y}} = \mathbf{C}\tilde{\mathbf{x}}+\mathbf{D}\tilde{\mathbf{u}} + \mathbf{r}_h(\tilde{\mathbf{x}},\tilde{\mathbf{u}}). \]
If \( (\mathbf{x}_0,\mathbf{u}_0) \) is an equilibrium, then \( \mathbf{f}(\mathbf{x}_0,\mathbf{u}_0)=\mathbf{0} \):. Neglecting higher-order remainders yields the linearized MIMO model:
\[ \boxed{ \dot{\tilde{\mathbf{x}}} = \mathbf{A}\tilde{\mathbf{x}}+\mathbf{B}\tilde{\mathbf{u}},\qquad \tilde{\mathbf{y}} = \mathbf{C}\tilde{\mathbf{x}}+\mathbf{D}\tilde{\mathbf{u}}.} \]
If \( (\mathbf{x}_0,\mathbf{u}_0) \) is not an equilibrium, the linearized dynamics contain an affine constant term \( \mathbf{f}(\mathbf{x}_0,\mathbf{u}_0) \). In practice, steady-state operating points are preferred because they remove this constant term and produce a standard LTI model.
5. Dimensions, Coupling, and Channel Interpretation
The Jacobian blocks have fixed dimensions: \( \mathbf{A}\in\mathbb{R}^{n\times n} \): state-to-state sensitivity, \( \mathbf{B}\in\mathbb{R}^{n\times m} \): input-to-state sensitivity, \( \mathbf{C}\in\mathbb{R}^{p\times n} \): state-to-output sensitivity, \( \mathbf{D}\in\mathbb{R}^{p\times m} \): direct input-to-output sensitivity.
For MIMO systems, coupling appears as off-diagonal structure: for example, if \( B_{12}\neq 0 \), then input channel 2 influences state 1 to first order. Similarly, \( C_{21}\neq 0 \) means state 1 influences output 2.
A common local question is: “Does input \( u_j \) immediately affect output \( y_i \)?” The first-order answer is determined by the feedthrough element \( D_{ij} \):. If \( D_{ij}=0 \), then any effect of \( u_j \) on \( y_i \) must pass through the state.
When \( \mathbf{D}\neq \mathbf{0} \), the linearized model is sometimes called proper with feedthrough. Physically, \( \mathbf{D}\neq \mathbf{0} \) can arise from sensor/actuator algebraic relations or measurement models that depend directly on inputs.
6. Transfer Matrix \( \mathbf{G}(s) \) of the Linearized Model
For the LTI perturbation model, and assuming zero initial perturbation \( \tilde{\mathbf{x}}(0)=\mathbf{0} \):, take Laplace transforms:
\[ s\tilde{\mathbf{X}}(s) = \mathbf{A}\tilde{\mathbf{X}}(s)+\mathbf{B}\tilde{\mathbf{U}}(s). \]
Solve for \( \tilde{\mathbf{X}}(s) \) (where \( \mathbf{I} \) is the \( n\times n \) identity):
\[ (s\mathbf{I}-\mathbf{A})\tilde{\mathbf{X}}(s) = \mathbf{B}\tilde{\mathbf{U}}(s) \quad \Rightarrow \quad \tilde{\mathbf{X}}(s) = (s\mathbf{I}-\mathbf{A})^{-1}\mathbf{B}\tilde{\mathbf{U}}(s). \]
Substitute into \( \tilde{\mathbf{Y}}(s)=\mathbf{C}\tilde{\mathbf{X}}(s)+\mathbf{D}\tilde{\mathbf{U}}(s) \):
\[ \tilde{\mathbf{Y}}(s) = \left(\mathbf{C}(s\mathbf{I}-\mathbf{A})^{-1}\mathbf{B}+\mathbf{D}\right)\tilde{\mathbf{U}}(s). \]
Hence the MIMO transfer matrix from \( \tilde{\mathbf{u}} \) to \( \tilde{\mathbf{y}} \) is
\[ \boxed{\mathbf{G}(s) = \mathbf{C}(s\mathbf{I}-\mathbf{A})^{-1}\mathbf{B}+\mathbf{D}\in\mathbb{R}^{p\times m}.} \]
Each element \( G_{ij}(s) \) is the (local) transfer function from input \( \tilde{u}_j \) to output \( \tilde{y}_i \), with all other inputs held at zero perturbation.
7. Computing \( \mathbf{A},\mathbf{B},\mathbf{C},\mathbf{D} \) in Practice
For models derived from physics, you often have explicit formulas for \( \mathbf{f} \) and \( \mathbf{h} \). Then the Jacobians can be computed:
- Symbolically (exact Jacobians) using CAS tools: SymPy (Python), Symbolic Math Toolbox (MATLAB), Mathematica.
- Numerically (approximate Jacobians) using finite differences in C++/Java or when formulas are complex.
A standard finite-difference approximation for \( \partial \mathbf{f}/\partial x_k \) uses a small step \( \epsilon \):
\[ \frac{\partial \mathbf{f}}{\partial x_k}(\mathbf{x}_0,\mathbf{u}_0) \approx \frac{\mathbf{f}(\mathbf{x}_0+\epsilon\mathbf{e}_k,\mathbf{u}_0)-\mathbf{f}(\mathbf{x}_0-\epsilon\mathbf{e}_k,\mathbf{u}_0)}{2\epsilon}, \]
where \( \mathbf{e}_k \) is the \( k \)-th unit vector. The same approach applies for \( \partial \mathbf{f}/\partial u_j \), \( \partial \mathbf{h}/\partial x_k \), and \( \partial \mathbf{h}/\partial u_j \).
The centered difference has truncation error of order \( O(\epsilon^2) \) (assuming sufficient smoothness), but excessively small \( \epsilon \) amplifies floating-point roundoff. In practice, \( \epsilon \) is chosen heuristically (e.g., \( 10^{-6} \) to \( 10^{-8} \) in normalized units), and results should be sanity-checked.
8. Python Lab — Symbolic Jacobians and Small-Signal Simulation
We demonstrate a 2-state, 2-input, 2-output nonlinear MIMO system:
\[ \begin{aligned} \dot{x}_1 &= -x_1 + \sin(x_2) + u_1,\\ \dot{x}_2 &= x_1^2 - 2x_2 + u_2,\\ y_1 &= x_1 + 0.2u_1,\\ y_2 &= x_2. \end{aligned} \]
At the equilibrium \( \mathbf{x}_0=[0\;\;0]^\top \), \( \mathbf{u}_0=[0\;\;0]^\top \), compute \( \mathbf{A},\mathbf{B},\mathbf{C},\mathbf{D} \) using SymPy. We then compare nonlinear and linearized responses for small inputs using SciPy.
import numpy as np
import sympy as sp
# --- Symbolic model definition ---
x1, x2, u1, u2 = sp.symbols('x1 x2 u1 u2', real=True)
x = sp.Matrix([x1, x2])
u = sp.Matrix([u1, u2])
f = sp.Matrix([
-x1 + sp.sin(x2) + u1,
x1**2 - 2*x2 + u2
])
h = sp.Matrix([
x1 + 0.2*u1,
x2
])
# Jacobian blocks
A_sym = f.jacobian(x)
B_sym = f.jacobian(u)
C_sym = h.jacobian(x)
D_sym = h.jacobian(u)
# Operating point (equilibrium)
op = {x1: 0.0, x2: 0.0, u1: 0.0, u2: 0.0}
A = np.array(A_sym.subs(op), dtype=float)
B = np.array(B_sym.subs(op), dtype=float)
C = np.array(C_sym.subs(op), dtype=float)
D = np.array(D_sym.subs(op), dtype=float)
print("A =\n", A)
print("B =\n", B)
print("C =\n", C)
print("D =\n", D)
# --- Build linear state-space model and simulate ---
# Libraries commonly used for system dynamics in Python:
# numpy, scipy.integrate for ODEs, sympy for symbolic Jacobians, and python-control for LTI models.
from scipy.integrate import solve_ivp
# Try to use python-control if available; otherwise fall back to direct simulation
try:
import control
sys = control.ss(A, B, C, D)
# Define a small input (piecewise constant) for local validation
def u_t(t):
return np.array([0.05*(t >= 0.5), -0.04*(t >= 1.0)], dtype=float)
# Nonlinear simulation
def f_nl(t, xvec):
uu = u_t(t)
return np.array([
-xvec[0] + np.sin(xvec[1]) + uu[0],
xvec[0]**2 - 2*xvec[1] + uu[1]
], dtype=float)
t_span = (0.0, 5.0)
t_eval = np.linspace(t_span[0], t_span[1], 1001)
x0 = np.zeros(2)
sol_nl = solve_ivp(f_nl, t_span, x0, t_eval=t_eval, rtol=1e-9, atol=1e-12)
# Linear simulation: xdot = A x + B u (perturbation variables)
# We'll simulate via explicit integration to keep dependencies minimal.
def f_lin(t, xvec):
uu = u_t(t)
return (A @ xvec + B @ uu)
sol_lin = solve_ivp(f_lin, t_span, x0, t_eval=t_eval, rtol=1e-12, atol=1e-14)
# Outputs
y_nl = np.vstack([
sol_nl.y[0] + 0.2*np.array([u_t(t)[0] for t in sol_nl.t]),
sol_nl.y[1]
])
y_lin = np.vstack([
(C @ sol_lin.y + D @ np.vstack([u_t(t) for t in sol_lin.t]).T)[0, :],
(C @ sol_lin.y + D @ np.vstack([u_t(t) for t in sol_lin.t]).T)[1, :]
])
# Report a simple local-error metric
err = np.max(np.abs(y_nl - y_lin))
print("Max |y_nonlinear - y_linear| over time =", err)
except Exception as e:
print("python-control not available or another error occurred:", e)
Interpretation at the operating point: \( \mathbf{A} \): contains the coupling term \( \cos(x_2) \) evaluated at \( x_2=0 \), hence \( A_{12}=1 \). This indicates that small changes in \( x_2 \) influence \( \dot{x}_1 \) linearly near the equilibrium. \( \mathbf{D} \): shows a direct path from \( u_1 \) to \( y_1 \).
9. C++ and Java Labs — Numerical Jacobians for MIMO Linearization
When symbolic expressions are unavailable (e.g., legacy code, lookup tables), you can compute \( \mathbf{A},\mathbf{B},\mathbf{C},\mathbf{D} \) numerically using centered finite differences. Below are compact reference implementations:
9.1 C++ (Eigen + RK4)
Common system-dynamics-related libraries in C++ include: Eigen (linear algebra), Boost::odeint (ODE integration), and custom solvers. The code below uses Eigen and a simple RK4 integrator.
#include <iostream>
#include <cmath>
#include <Eigen/Dense>
// Nonlinear MIMO model (same as Python example)
// xdot = f(x,u), y = h(x,u)
static Eigen::VectorXd f_nl(const Eigen::VectorXd& x, const Eigen::VectorXd& u) {
Eigen::VectorXd dx(2);
dx(0) = -x(0) + std::sin(x(1)) + u(0);
dx(1) = x(0)*x(0) - 2.0*x(1) + u(1);
return dx;
}
static Eigen::VectorXd h_nl(const Eigen::VectorXd& x, const Eigen::VectorXd& u) {
Eigen::VectorXd y(2);
y(0) = x(0) + 0.2*u(0);
y(1) = x(1);
return y;
}
// Centered finite-difference Jacobian wrt x
static Eigen::MatrixXd jacobian_x(
const Eigen::VectorXd& x0, const Eigen::VectorXd& u0, double eps,
Eigen::VectorXd (*F)(const Eigen::VectorXd&, const Eigen::VectorXd&)) {
const int n = x0.size();
const int q = F(x0,u0).size();
Eigen::MatrixXd J(q, n);
for (int k = 0; k < n; ++k) {
Eigen::VectorXd e = Eigen::VectorXd::Zero(n);
e(k) = 1.0;
Eigen::VectorXd Fp = F(x0 + eps*e, u0);
Eigen::VectorXd Fm = F(x0 - eps*e, u0);
J.col(k) = (Fp - Fm) / (2.0*eps);
}
return J;
}
// Centered finite-difference Jacobian wrt u
static Eigen::MatrixXd jacobian_u(
const Eigen::VectorXd& x0, const Eigen::VectorXd& u0, double eps,
Eigen::VectorXd (*F)(const Eigen::VectorXd&, const Eigen::VectorXd&)) {
const int m = u0.size();
const int q = F(x0,u0).size();
Eigen::MatrixXd J(q, m);
for (int j = 0; j < m; ++j) {
Eigen::VectorXd e = Eigen::VectorXd::Zero(m);
e(j) = 1.0;
Eigen::VectorXd Fp = F(x0, u0 + eps*e);
Eigen::VectorXd Fm = F(x0, u0 - eps*e);
J.col(j) = (Fp - Fm) / (2.0*eps);
}
return J;
}
// Simple RK4 integrator step: x_{k+1} = x_k + (dt/6)(k1+2k2+2k3+k4)
static Eigen::VectorXd rk4_step(
const Eigen::VectorXd& x, const Eigen::VectorXd& u, double dt,
Eigen::VectorXd (*f)(const Eigen::VectorXd&, const Eigen::VectorXd&)) {
Eigen::VectorXd k1 = f(x, u);
Eigen::VectorXd k2 = f(x + 0.5*dt*k1, u);
Eigen::VectorXd k3 = f(x + 0.5*dt*k2, u);
Eigen::VectorXd k4 = f(x + dt*k3, u);
return x + (dt/6.0)*(k1 + 2.0*k2 + 2.0*k3 + k4);
}
int main() {
Eigen::VectorXd x0(2); x0 << 0.0, 0.0;
Eigen::VectorXd u0(2); u0 << 0.0, 0.0;
double eps = 1e-7;
Eigen::MatrixXd A = jacobian_x(x0, u0, eps, f_nl);
Eigen::MatrixXd B = jacobian_u(x0, u0, eps, f_nl);
Eigen::MatrixXd C = jacobian_x(x0, u0, eps, h_nl);
Eigen::MatrixXd D = jacobian_u(x0, u0, eps, h_nl);
std::cout << "A=\n" << A << "\n\n";
std::cout << "B=\n" << B << "\n\n";
std::cout << "C=\n" << C << "\n\n";
std::cout << "D=\n" << D << "\n\n";
// Local validation: simulate nonlinear and linearized (perturbation) models
auto u_of_t = [](double t) {
Eigen::VectorXd u(2);
u(0) = (t >= 0.5) ? 0.05 : 0.0;
u(1) = (t >= 1.0) ? -0.04 : 0.0;
return u;
};
double dt = 0.001;
double T = 5.0;
int N = static_cast<int>(T/dt);
Eigen::VectorXd x_nl = x0;
Eigen::VectorXd x_lin = Eigen::VectorXd::Zero(2);
double max_err = 0.0;
for (int k = 0; k <= N; ++k) {
double t = k*dt;
Eigen::VectorXd u = u_of_t(t);
// Nonlinear state (absolute variables)
x_nl = rk4_step(x_nl, u, dt, f_nl);
// Linearized perturbation: x_tilde_dot = A x_tilde + B u_tilde (here u_tilde=u)
auto f_lin = [&](const Eigen::VectorXd& xt) {
return A*xt + B*u;
};
// RK4 for linear model
Eigen::VectorXd k1 = f_lin(x_lin);
Eigen::VectorXd k2 = f_lin(x_lin + 0.5*dt*k1);
Eigen::VectorXd k3 = f_lin(x_lin + 0.5*dt*k2);
Eigen::VectorXd k4 = f_lin(x_lin + dt*k3);
x_lin = x_lin + (dt/6.0)*(k1 + 2.0*k2 + 2.0*k3 + k4);
// Outputs
Eigen::VectorXd y_nl = h_nl(x_nl, u);
Eigen::VectorXd y_lin = C*x_lin + D*u;
max_err = std::max(max_err, (y_nl - y_lin).cwiseAbs().maxCoeff());
}
std::cout << "Max |y_nonlinear - y_linear| = " << max_err << "\n";
return 0;
}
9.2 Java (EJML for Matrices)
In Java, common choices are EJML (linear algebra) and Apache Commons Math (ODE solvers). Below is a compact EJML-based Jacobian computation and RK4 simulation skeleton.
import org.ejml.data.DMatrixRMaj;
import org.ejml.dense.row.CommonOps_DDRM;
public class MimoLinearization {
// Nonlinear dynamics f(x,u) for 2-state, 2-input example
static double[] f_nl(double[] x, double[] u) {
return new double[] {
-x[0] + Math.sin(x[1]) + u[0],
x[0]*x[0] - 2.0*x[1] + u[1]
};
}
// Output map h(x,u)
static double[] h_nl(double[] x, double[] u) {
return new double[] {
x[0] + 0.2*u[0],
x[1]
};
}
// Centered finite-difference Jacobian wrt x
static DMatrixRMaj jacobianX(double[] x0, double[] u0, double eps, boolean dynamics) {
int n = x0.length;
double[] F0 = dynamics ? f_nl(x0,u0) : h_nl(x0,u0);
int q = F0.length;
DMatrixRMaj J = new DMatrixRMaj(q, n);
for (int k = 0; k < n; k++) {
double[] xp = x0.clone(); xp[k] += eps;
double[] xm = x0.clone(); xm[k] -= eps;
double[] Fp = dynamics ? f_nl(xp,u0) : h_nl(xp,u0);
double[] Fm = dynamics ? f_nl(xm,u0) : h_nl(xm,u0);
for (int i = 0; i < q; i++) {
J.set(i, k, (Fp[i]-Fm[i])/(2.0*eps));
}
}
return J;
}
// Centered finite-difference Jacobian wrt u
static DMatrixRMaj jacobianU(double[] x0, double[] u0, double eps, boolean dynamics) {
int m = u0.length;
double[] F0 = dynamics ? f_nl(x0,u0) : h_nl(x0,u0);
int q = F0.length;
DMatrixRMaj J = new DMatrixRMaj(q, m);
for (int j = 0; j < m; j++) {
double[] up = u0.clone(); up[j] += eps;
double[] um = u0.clone(); um[j] -= eps;
double[] Fp = dynamics ? f_nl(x0,up) : h_nl(x0,up);
double[] Fm = dynamics ? f_nl(x0,um) : h_nl(x0,um);
for (int i = 0; i < q; i++) {
J.set(i, j, (Fp[i]-Fm[i])/(2.0*eps));
}
}
return J;
}
// RK4 step for xdot = f(x,u)
static double[] rk4Step(double[] x, double[] u, double dt) {
double[] k1 = f_nl(x,u);
double[] x2 = new double[] { x[0] + 0.5*dt*k1[0], x[1] + 0.5*dt*k1[1] };
double[] k2 = f_nl(x2,u);
double[] x3 = new double[] { x[0] + 0.5*dt*k2[0], x[1] + 0.5*dt*k2[1] };
double[] k3 = f_nl(x3,u);
double[] x4 = new double[] { x[0] + dt*k3[0], x[1] + dt*k3[1] };
double[] k4 = f_nl(x4,u);
return new double[] {
x[0] + (dt/6.0)*(k1[0] + 2*k2[0] + 2*k3[0] + k4[0]),
x[1] + (dt/6.0)*(k1[1] + 2*k2[1] + 2*k3[1] + k4[1])
};
}
public static void main(String[] args) {
double[] x0 = new double[] {0.0, 0.0};
double[] u0 = new double[] {0.0, 0.0};
double eps = 1e-7;
DMatrixRMaj A = jacobianX(x0,u0,eps,true);
DMatrixRMaj B = jacobianU(x0,u0,eps,true);
DMatrixRMaj C = jacobianX(x0,u0,eps,false);
DMatrixRMaj D = jacobianU(x0,u0,eps,false);
System.out.println("A=\n" + A);
System.out.println("B=\n" + B);
System.out.println("C=\n" + C);
System.out.println("D=\n" + D);
// Simulate linear perturbation: xdot = A x + B u
double dt = 0.001;
double T = 5.0;
int N = (int)(T/dt);
double[] xLin = new double[] {0.0, 0.0};
double[] xNl = x0.clone();
double maxErr = 0.0;
for (int k = 0; k <= N; k++) {
double t = k*dt;
double[] u = new double[] {
(t >= 0.5) ? 0.05 : 0.0,
(t >= 1.0) ? -0.04 : 0.0
};
// Nonlinear step
xNl = rk4Step(xNl, u, dt);
// Linear step (RK4 with matrix ops)
// k1 = A x + B u, etc.
double[] k1 = linDyn(A,B,xLin,u);
double[] x2 = new double[] { xLin[0] + 0.5*dt*k1[0], xLin[1] + 0.5*dt*k1[1] };
double[] k2 = linDyn(A,B,x2,u);
double[] x3 = new double[] { xLin[0] + 0.5*dt*k2[0], xLin[1] + 0.5*dt*k2[1] };
double[] k3 = linDyn(A,B,x3,u);
double[] x4 = new double[] { xLin[0] + dt*k3[0], xLin[1] + dt*k3[1] };
double[] k4 = linDyn(A,B,x4,u);
xLin[0] += (dt/6.0)*(k1[0] + 2*k2[0] + 2*k3[0] + k4[0]);
xLin[1] += (dt/6.0)*(k1[1] + 2*k2[1] + 2*k3[1] + k4[1]);
double[] yNl = h_nl(xNl, u);
double[] yLin = linOut(C,D,xLin,u);
maxErr = Math.max(maxErr, Math.max(Math.abs(yNl[0]-yLin[0]), Math.abs(yNl[1]-yLin[1])));
}
System.out.println("Max |y_nonlinear - y_linear| = " + maxErr);
}
static double[] linDyn(DMatrixRMaj A, DMatrixRMaj B, double[] x, double[] u) {
// dx = A x + B u
double dx0 = A.get(0,0)*x[0] + A.get(0,1)*x[1] + B.get(0,0)*u[0] + B.get(0,1)*u[1];
double dx1 = A.get(1,0)*x[0] + A.get(1,1)*x[1] + B.get(1,0)*u[0] + B.get(1,1)*u[1];
return new double[] {dx0, dx1};
}
static double[] linOut(DMatrixRMaj C, DMatrixRMaj D, double[] x, double[] u) {
// y = C x + D u
double y0 = C.get(0,0)*x[0] + C.get(0,1)*x[1] + D.get(0,0)*u[0] + D.get(0,1)*u[1];
double y1 = C.get(1,0)*x[0] + C.get(1,1)*x[1] + D.get(1,0)*u[0] + D.get(1,1)*u[1];
return new double[] {y0, y1};
}
}
10. MATLAB/Simulink Lab — Linear Model Extraction and Validation
MATLAB provides symbolic Jacobians (Symbolic Math Toolbox) and standard
state-space tools (Control System Toolbox). Simulink additionally
supports extracting linear models around operating points using
linmod (legacy) or linearize (modern
workflows; may require Simulink Control Design depending on usage).
10.1 MATLAB symbolic Jacobians
% Define symbolic variables
syms x1 x2 u1 u2 real
x = [x1; x2];
u = [u1; u2];
% Nonlinear model
f = [-x1 + sin(x2) + u1;
x1^2 - 2*x2 + u2];
h = [x1 + 0.2*u1;
x2];
% Jacobians
A_sym = jacobian(f, x);
B_sym = jacobian(f, u);
C_sym = jacobian(h, x);
D_sym = jacobian(h, u);
% Operating point
x0 = [0; 0];
u0 = [0; 0];
A = double(subs(A_sym, [x1 x2 u1 u2], [x0.' u0.']));
B = double(subs(B_sym, [x1 x2 u1 u2], [x0.' u0.']));
C = double(subs(C_sym, [x1 x2 u1 u2], [x0.' u0.']));
D = double(subs(D_sym, [x1 x2 u1 u2], [x0.' u0.']));
% Linearized perturbation model
sys = ss(A,B,C,D);
disp(A); disp(B); disp(C); disp(D);
10.2 Simulink workflow (model-based linearization)
In Simulink, you typically (i) build the nonlinear block model, (ii)
specify the operating point, then (iii) extract \(
\mathbf{A},\mathbf{B},\mathbf{C},\mathbf{D} \) and export an
ss object for analysis.
flowchart TD
M1["Build block model of f,h"] --> OP["Set operating point (x0,u0)"]
OP --> TR["Select linearization I/O points"]
TR --> LIN["Compute linear model (A,B,C,D)"]
LIN --> EXP["Export as state-space object"]
EXP --> ANA["Analyze with LTI tools"]
ANA --> VER["Validate locally by simulation"]
A minimal script pattern (exact commands depend on your Simulink configuration and model name) is:
% Suppose the Simulink model is named: 'mimo_nonlinear_model'
% and is already built and saved.
model = 'mimo_nonlinear_model';
load_system(model);
% Option A (legacy): linmod (works for many continuous models)
% [A,B,C,D] = linmod(model, x0, u0);
% sys = ss(A,B,C,D);
% Option B (operating point + linearize):
% op = operpoint(model); % or create using findop
% sys = linearize(model, op); % returns ss model
% After obtaining sys:
% step(sys) or lsim(sys, u, t) can be used for LTI analysis.
Recommendation: always compare nonlinear vs linearized outputs for small perturbations to confirm the local validity expected from Lesson 3.
11. Wolfram Mathematica Lab — Jacobian Blocks and Transfer Matrix
Mathematica can compute Jacobians exactly and manipulate state-space models symbolically.
(* Define variables *)
x = {x1, x2};
u = {u1, u2};
(* Nonlinear model *)
f = {
-x1 + Sin[x2] + u1,
x1^2 - 2 x2 + u2
};
h = {
x1 + 0.2 u1,
x2
};
(* Jacobian blocks *)
A = D[f, {x}];
B = D[f, {u}];
C = D[h, {x}];
Dmat = D[h, {u}];
(* Operating point *)
op = {x1 -> 0, x2 -> 0, u1 -> 0, u2 -> 0};
A0 = A /. op // N;
B0 = B /. op // N;
C0 = C /. op // N;
D0 = Dmat /. op // N;
A0
B0
C0
D0
(* Create linear state-space model and compute transfer matrix *)
sys = StateSpaceModel[{A0, B0, C0, D0}];
G = TransferFunctionModel[sys];
G
The resulting transfer matrix \( \mathbf{G}(s) \) is the local MIMO approximation mapping \( \tilde{\mathbf{u}} → \tilde{\mathbf{y}} \) in the Laplace domain.
12. Problems and Solutions
Problem 1 (Block Jacobians for a MIMO model): Consider
\[ \dot{\mathbf{x}}= \begin{bmatrix} -x_1 + \sin(x_2) + u_1\\ x_1^2 - 2x_2 + u_2 \end{bmatrix}, \qquad \mathbf{y}= \begin{bmatrix} x_1 + 0.2u_1\\ x_2 \end{bmatrix}. \]
Compute \( \mathbf{A},\mathbf{B},\mathbf{C},\mathbf{D} \) at the equilibrium \( \mathbf{x}_0=\begin{bmatrix}0\\0\end{bmatrix} \), \( \mathbf{u}_0=\begin{bmatrix}0\\0\end{bmatrix} \).
Solution: By definition, \( \mathbf{A}=\left.\dfrac{\partial \mathbf{f}}{\partial \mathbf{x}}\right|_{(\mathbf{x}_0,\mathbf{u}_0)} \), \( \mathbf{B}=\left.\dfrac{\partial \mathbf{f}}{\partial \mathbf{u}}\right|_{(\mathbf{x}_0,\mathbf{u}_0)} \), \( \mathbf{C}=\left.\dfrac{\partial \mathbf{h}}{\partial \mathbf{x}}\right|_{(\mathbf{x}_0,\mathbf{u}_0)} \), \( \mathbf{D}=\left.\dfrac{\partial \mathbf{h}}{\partial \mathbf{u}}\right|_{(\mathbf{x}_0,\mathbf{u}_0)} \).
\[ \frac{\partial \mathbf{f}}{\partial \mathbf{x}} = \begin{bmatrix} \dfrac{\partial}{\partial x_1}(-x_1+\sin x_2+u_1) & \dfrac{\partial}{\partial x_2}(-x_1+\sin x_2+u_1)\\[6pt] \dfrac{\partial}{\partial x_1}(x_1^2-2x_2+u_2) & \dfrac{\partial}{\partial x_2}(x_1^2-2x_2+u_2) \end{bmatrix} = \begin{bmatrix} -1 & \cos(x_2)\\ 2x_1 & -2 \end{bmatrix}. \]
\[ \Rightarrow\quad \mathbf{A} = \begin{bmatrix} -1 & \cos(0)\\ 2\cdot 0 & -2 \end{bmatrix} = \begin{bmatrix} -1 & 1\\ 0 & -2 \end{bmatrix}. \]
\[ \mathbf{B} = \left. \frac{\partial \mathbf{f}}{\partial \mathbf{u}} \right|_{(\mathbf{x}_0,\mathbf{u}_0)} = \begin{bmatrix} \dfrac{\partial}{\partial u_1}(\cdot) & \dfrac{\partial}{\partial u_2}(\cdot)\\ \dfrac{\partial}{\partial u_1}(\cdot) & \dfrac{\partial}{\partial u_2}(\cdot) \end{bmatrix} = \begin{bmatrix} 1 & 0\\ 0 & 1 \end{bmatrix}. \]
\[ \mathbf{C} = \left. \frac{\partial \mathbf{h}}{\partial \mathbf{x}} \right|_{(\mathbf{x}_0,\mathbf{u}_0)} = \begin{bmatrix} 1 & 0\\ 0 & 1 \end{bmatrix}, \qquad \mathbf{D} = \left. \frac{\partial \mathbf{h}}{\partial \mathbf{u}} \right|_{(\mathbf{x}_0,\mathbf{u}_0)} = \begin{bmatrix} 0.2 & 0\\ 0 & 0 \end{bmatrix}. \]
Problem 2 (Why equilibria remove the affine term): Show that if \( \mathbf{f}(\mathbf{x}_0,\mathbf{u}_0)=\mathbf{0} \), then the linearized perturbation dynamics have no constant term.
Solution: From Section 4,
\[ \dot{\tilde{\mathbf{x}}} = \mathbf{f}(\mathbf{x}_0,\mathbf{u}_0)+\mathbf{A}\tilde{\mathbf{x}}+\mathbf{B}\tilde{\mathbf{u}}+\mathbf{r}_f(\tilde{\mathbf{x}},\tilde{\mathbf{u}}). \]
If \( (\mathbf{x}_0,\mathbf{u}_0) \) is an equilibrium, then \( \mathbf{f}(\mathbf{x}_0,\mathbf{u}_0)=\mathbf{0} \). Neglecting higher-order terms yields
\[ \dot{\tilde{\mathbf{x}}}=\mathbf{A}\tilde{\mathbf{x}}+\mathbf{B}\tilde{\mathbf{u}}, \]
which has no affine offset. □
Problem 3 (Transfer matrix derivation): Starting from \( \dot{\tilde{\mathbf{x}}}=\mathbf{A}\tilde{\mathbf{x}}+\mathbf{B}\tilde{\mathbf{u}} \), \( \tilde{\mathbf{y}}=\mathbf{C}\tilde{\mathbf{x}}+\mathbf{D}\tilde{\mathbf{u}} \), and \( \tilde{\mathbf{x}}(0)=\mathbf{0} \), derive \( \tilde{\mathbf{Y}}(s)=\mathbf{G}(s)\tilde{\mathbf{U}}(s) \) and provide \( \mathbf{G}(s) \).
Solution: Taking Laplace transforms gives
\[ s\tilde{\mathbf{X}}(s)=\mathbf{A}\tilde{\mathbf{X}}(s)+\mathbf{B}\tilde{\mathbf{U}}(s) \Rightarrow (s\mathbf{I}-\mathbf{A})\tilde{\mathbf{X}}(s)=\mathbf{B}\tilde{\mathbf{U}}(s). \]
\[ \tilde{\mathbf{X}}(s)=(s\mathbf{I}-\mathbf{A})^{-1}\mathbf{B}\tilde{\mathbf{U}}(s). \]
Substitute into the output equation:
\[ \tilde{\mathbf{Y}}(s) = \left(\mathbf{C}(s\mathbf{I}-\mathbf{A})^{-1}\mathbf{B}+\mathbf{D}\right)\tilde{\mathbf{U}}(s), \]
hence \( \mathbf{G}(s)=\mathbf{C}(s\mathbf{I}-\mathbf{A})^{-1}\mathbf{B}+\mathbf{D} \). □
Problem 4 (Coordinate change and linearization): Let \( \tilde{\mathbf{x}}=\mathbf{T}\tilde{\mathbf{z}} \) where \( \mathbf{T}\in\mathbb{R}^{n\times n} \) is invertible. Show that the linearized matrices transform as \( \mathbf{A}_z=\mathbf{T}^{-1}\mathbf{A}\mathbf{T} \) and \( \mathbf{B}_z=\mathbf{T}^{-1}\mathbf{B} \), while \( \mathbf{C}_z=\mathbf{C}\mathbf{T} \) and \( \mathbf{D}_z=\mathbf{D} \).
Solution: Substitute \( \tilde{\mathbf{x}}=\mathbf{T}\tilde{\mathbf{z}} \) into \( \dot{\tilde{\mathbf{x}}}=\mathbf{A}\tilde{\mathbf{x}}+\mathbf{B}\tilde{\mathbf{u}} \):
\[ \mathbf{T}\dot{\tilde{\mathbf{z}}}=\mathbf{A}\mathbf{T}\tilde{\mathbf{z}}+\mathbf{B}\tilde{\mathbf{u}} \Rightarrow \dot{\tilde{\mathbf{z}}}=\mathbf{T}^{-1}\mathbf{A}\mathbf{T}\tilde{\mathbf{z}}+\mathbf{T}^{-1}\mathbf{B}\tilde{\mathbf{u}}. \]
So \( \mathbf{A}_z=\mathbf{T}^{-1}\mathbf{A}\mathbf{T} \) and \( \mathbf{B}_z=\mathbf{T}^{-1}\mathbf{B} \). For outputs, \( \tilde{\mathbf{y}}=\mathbf{C}\tilde{\mathbf{x}}+\mathbf{D}\tilde{\mathbf{u}}= \mathbf{C}\mathbf{T}\tilde{\mathbf{z}}+\mathbf{D}\tilde{\mathbf{u}} \), hence \( \mathbf{C}_z=\mathbf{C}\mathbf{T} \), \( \mathbf{D}_z=\mathbf{D} \). □
Problem 5 (Local error order): Using the remainder bound from Section 3, argue that for sufficiently small perturbations, the modeling error between the nonlinear model and the linearized model scales like \( O(\|\tilde{\mathbf{z}}\|^2) \).
Solution: From Section 3,
\[ \mathbf{f}(\mathbf{z}_0+\tilde{\mathbf{z}}) = \mathbf{f}(\mathbf{z}_0)+\mathbf{J}_f(\mathbf{z}_0)\tilde{\mathbf{z}}+\mathbf{r}_f(\tilde{\mathbf{z}}), \]
and if \( \mathbf{J}_f \) is locally Lipschitz with constant \( L \), then \( \|\mathbf{r}_f(\tilde{\mathbf{z}})\|\le \tfrac{L}{2}\|\tilde{\mathbf{z}}\|^2 \). Neglecting \( \mathbf{r}_f \) is therefore a second-order approximation, meaning the first-order linear model error decreases quadratically as perturbations shrink. □
13. Summary
We extended Jacobian linearization to MIMO nonlinear systems by (i) framing the system as maps \( \mathbf{f}(\mathbf{x},\mathbf{u}) \) and \( \mathbf{h}(\mathbf{x},\mathbf{u}) \), (ii) using perturbation variables around an operating point, (iii) deriving the small-signal model with Jacobian blocks \( \mathbf{A},\mathbf{B},\mathbf{C},\mathbf{D} \), and (iv) connecting the result to the transfer matrix \( \mathbf{G}(s)=\mathbf{C}(s\mathbf{I}-\mathbf{A})^{-1}\mathbf{B}+\mathbf{D} \). We also provided rigorous remainder bounds and practical computation routes (symbolic and finite-difference) across multiple programming environments.
14. References
- Hartman, P. (1960). A lemma in the theory of structural stability of differential equations. Proceedings of the American Mathematical Society, 11(4), 610–620.
- Grobman, D.M. (1959). Homeomorphism of systems of differential equations. Doklady Akademii Nauk SSSR, 128, 880–881.
- Kalman, R.E. (1960). A new approach to linear filtering and prediction problems. Journal of Basic Engineering (ASME), 82(1), 35–45.
- Jurdjevic, V., & Sussmann, H.J. (1972). Controllability of nonlinear systems. Journal of Differential Equations, 12(1), 95–116.
- Hermann, R., & Krener, A.J. (1977). Nonlinear controllability and observability. IEEE Transactions on Automatic Control, 22(5), 728–740.
- Zabczyk, J. (1979). Some remarks on controllability of nonlinear systems. SIAM Journal on Control and Optimization, 17(2), 237–246.
- Sontag, E.D. (1983). A Lyapunov-like characterization of asymptotic controllability. SIAM Journal on Control and Optimization, 21(3), 462–471.