Skip to content

System Identification

Practical

System Identification is how you build a math model of a system from measured data instead of deriving it from first principles. You poke the system with known inputs, record the outputs, and fit a model that reproduces that behavior. It matters because model-based controllers like MPC and LQR need a model — and sysid is how you get one when the physics are too messy to hand-derive.

The Recipe

  1. Excite the system with a rich input — a step, a chirp (sweeping frequency), or a PRBS (pseudo-random binary signal) that stirs up many dynamics at once.
  2. Log the input and output data, time-aligned.
  3. Pick a model structure — how many states, linear or nonlinear.
  4. Fit parameters to minimize prediction error (least squares is the workhorse).
  5. Validate on held-out data the model never saw during fitting.

Grey-box vs Black-box

  • Grey-box: you know the structure from physics (e.g. a first-order motor model) and only fit the unknown constants — a time constant, a friction coefficient. Fewer parameters, more trustworthy extrapolation.
  • Black-box: you let a generic model (ARX, a state-space form, or a neural net) learn the input→output mapping with no physical assumptions. More flexible, but easier to overfit and harder to interpret.

In robotics, sysid identifies motor and actuator models, measures friction and payload, calibrates a sim to match the real robot (the heart of Sim-to-Real Transfer), and supplies the dynamics model that MPC depends on.

Prerequisites

How It Works

known input REAL SYSTEM measured output
u (step/chirp/PRBS) (unknown physics) y (with noise)
───────────────────► ┌──────────────────┐ ───────────────────►
│ ??? motor + load │
└──────────────────┘
│ fit parameters to
│ minimize ||y - ŷ||²
▼
┌──────────────────┐
same input ────────► │ FITTED MODEL │ ─────► ŷ ≈ y
│ ŷ[k+1]=a·y+b·u │ (predicted output)
└──────────────────┘

Runnable Example

Identify a first-order system y[k+1] = a·y[k] + b·u[k] from step-response data. We generate synthetic data from known a and b, add small noise, then recover them with numpy least squares.

import numpy as np
rng = np.random.default_rng(0)
# --- Ground truth (the "real system" we pretend not to know) ---
a_true, b_true = 0.8, 0.5
# --- 1. Excite with a step input, simulate, add measurement noise ---
N = 200
u = np.zeros(N)
u[10:] = 1.0 # step turns on at k=10
y = np.zeros(N)
for k in range(N - 1):
y[k + 1] = a_true * y[k] + b_true * u[k]
y_meas = y + rng.normal(0, 0.01, size=N) # noisy measurements
# --- 2. Build the regression: y[k+1] = [y[k], u[k]] @ [a, b] ---
Y_next = y_meas[1:]
Phi = np.column_stack([y_meas[:-1], u[:-1]])
# --- 3. Fit on the first 70%, validate on the unseen rest ---
split = int(0.7 * (N - 1))
theta, *_ = np.linalg.lstsq(Phi[:split], Y_next[:split], rcond=None)
a_hat, b_hat = theta
# --- 4. Report recovered params and held-out error ---
y_pred = Phi[split:] @ theta
rmse = np.sqrt(np.mean((y_pred - Y_next[split:]) ** 2))
print(f"a: true={a_true}, recovered={a_hat:.4f}")
print(f"b: true={b_true}, recovered={b_hat:.4f}")
print(f"validation RMSE (held-out): {rmse:.4f}")
# Output:
# a: true=0.8, recovered=0.8001
# b: true=0.5, recovered=0.4999
# validation RMSE (held-out): 0.0118

The recovered a and b match the true values to within noise, and the small held-out RMSE confirms the model generalizes rather than memorizing.

Sources