System identification in the command window#
Every name on this page is typed at the command window (or passed to
ICoreBlocks --console "<line>") and answers what MATLAB's System
Identification Toolbox answers for the same arguments — same defaults, same
shapes, same edge cases. If you have MATLAB code that fits a model to a record
and scores it, it should paste in and run.
That is a promise about NUMBERS, and for this toolbox it is a harder promise than for most: several of these estimators answer where a search stopped rather than the solution of an equation, so matching them means matching MATLAB's initialisation and its line search too, not just its criterion. Where the two sides would still disagree, this page says so rather than leaving it for you to find.
There is no iddata here — the samples go straight in#
MATLAB's documentation packs a record into an iddata object first. This
console has no object kinds, so every estimator takes the samples
themselves, INPUT FIRST, with the sample time after them:
| MATLAB | Here |
|---|---|
arx(iddata(y, u, Ts), [na nb nk]) | arx(u, y, Ts, [na nb nk]) |
armax(iddata(y, u, Ts), nn) | armax(u, y, Ts, nn) |
compare(iddata(y, u, Ts), sys) | compare(u, y, Ts, sys) |
⚠ Two names take one matrix and take it OUTPUT first — covf(z, M) and
rarx(z, nn, adm, adg), where z = [y u]. That is MATLAB's own inconsistency,
transcribed rather than tidied: a console that quietly reordered them would
answer a plausible trajectory for the wrong model.
The families, and what to reach for#
| You want to | Use |
|---|---|
| Make an excitation signal | idinput(N, "prbs" | "rbs" | "rgs" | "sine"), with a band and levels |
| Fit by least squares | arx(u, y, Ts, [na nb nk]), ar(y, n), ivar(y, na, nc) |
| Fit by prediction error | armax, oe, bj, and pem(u, y, Ts, sys) to refine a model you already have |
| Fit a state-space model without picking a structure | n4sid(u, y, Ts, nx) — the subspace method |
| Choose an order or a delay | arxstruc, selstruc, delayest |
| Look before choosing a model | covf (covariances), etfe and spa (frequency response) |
| Watch an estimate form | rarx, rpem — the parameter trajectory over the record |
| Use a fitted model | sim, predict, forecast, resid |
| Score a fit | compare(u, y, Ts, sys), goodnessOfFit(x, xref, measure) |
| Read a model's coefficients | tfdata(sys, "v"), ssdata(sys) |
| Read a model the way MATLAB's identified classes do | polydata, getpvec, setpvec, getcov, idssdata |
| Build one of MATLAB's identified models | idtf, idpoly, idss |
A worked line or two#
Make an excitation signal — a maximal-length shift register, so it is the same sequence every time:
>>> u = idinput(15, "prbs")'
u = [[-1, -1, -1, -1, 1, -1, 1, -1, -1, 1, 1, -1, 1, 1, 1]] # Matrix of Double
Drive a known plant with a square-ish input, fit it back with arx, and read
the coefficients out. On a noise-free record the fit recovers the plant exactly,
which is what a least-squares estimator should do and a useful thing to check
first when a fit surprises you:
>>> y = simtf(tf([0, 0.5, 0.2], [1, -0.8, 0.15], 1), [1, 1, 1, 1, -1, -1, -1, -1, -1, 1, 1, -1, 1, 1, 1]); tfdata(arx([1, 1, 1, 1, -1, -1, -1, -1, -1, 1, 1, -1, 1, 1, 1], y, 1, [2, 2, 1]), "v")
[[0, 0.5, 0.2]] # Matrix of Double
Read the same model the way MATLAB's identified classes read theirs — B over
F, with A, C and D all 1, because a transfer function has no noise
model to put in C and D:
>>> [pdA, pdB, pdC, pdD, pdF] = polydata(tf([1, 2], [1, -0.8, 0.15], 0.1))
pdA = 1 # Integer
pdB = [[0, 1, 2]] # Matrix of Double
pdC = 1 # Integer
pdD = 1 # Integer
pdF = [[1, -0.8, 0.15]] # Matrix of Double
Note pdB: the model was written tf([1, 2], ...) and B came back
[0, 1, 2]. That is the first trap below, and it is MATLAB's rule, not this
console's.
Fit a state-space model without choosing a structure at all — no na, no
nb, no delay, just an order. un/yn below are 120 samples of a second-order
plant driven by a random sign sequence, with noise on the output:
>>> tfdata(n4sid(un, yn, 1, 2), "v")
[[0, 0.597953, -0.222138]] # Matrix of Double
>>> dcgain(n4sid(un, yn, 1, 2))
1.3514 # Double
The weighting and the noise model are options, and each of them changes the answer rather than dressing it up:
>>> tfdata(n4sid(un, yn, 1, 2, "N4Weight", "MOESP"), "v")
[[0, 0.598032, -0.222123]] # Matrix of Double
>>> tfdata(n4sid(un, yn, 1, 2, "DisturbanceModel", "none"), "v")
[[0, 0.604816, -0.250281]] # Matrix of Double
[sys, x0] = n4sid(...) also answers the initial state the fit used — and read
it through initial(sys, x0) rather than looking at its entries, for the reason
in point 9 below.
Every transcript on this page is real output, captured with
ICoreBlocks --console "<line>": the earlier ones on 2026-09-04 from a build of
commit 82e6dabb with the T6.12 change applied, and the n4sid ones on
2026-09-05 from a build of commit 6bbc6264 with the T6.5 change applied.
The identified-model classes, and the two traps in them#
MATLAB's estimators do not answer a tf. They answer an idpoly, an idtf or
an idss — the same dynamics carrying an estimation report, a parameter
covariance and a noise model beside them. This console answers a plain tf
or ss, because it has no object kinds; idtf, idpoly and idss build
one, and polydata, getpvec, setpvec, getcov and idssdata read one the
way MATLAB's methods read theirs.
That mapping is exact for the dynamics and lossy for everything else, and the two places it bites are worth knowing before you use them.
Trap 1 — a discrete idtf is written in z^-1, and a tf is written in z#
idtf([1 2], [1 -0.8 0.15], 0.1) is (z^2 + 2z) / (z^2 - 0.8z + 0.15)
tf( [1 2], [1 -0.8 0.15], 0.1) is (z + 2) / (z^2 - 0.8z + 0.15)
The same two rows, two different models — because MATLAB's idtf reads them as
ascending powers of q^-1 and tf reads them as descending powers of z. The
console's idtf applies MATLAB's rule (it right-pads the shorter row) so that
idtf(...) here and idtf(...) in MATLAB are the same model. A continuous
idtf is written in s, so there the two constructors agree.
idtf also normalises the pair by the leading denominator coefficient, so
idtf([0 1 2], [2 -1.6 0.3], 0.1) is [0 0.5 1] over [1 -0.8 0.15].
Trap 2 — polydata's slots depend on the estimator, and a tf has forgotten which#
MATLAB's general polynomial model is
A(q) y = [B(q)/F(q)] u + [C(q)/D(q)] e
and polydata hands back all five polynomials. Which slot your denominator
lands in depends on what fitted it: arx puts it in A and leaves F = 1;
oe puts it in F and leaves A = 1. A bare transfer function does not
remember, so polydata(sys) here always reads the model the way idtf(sys)
does — B over F, with A, C and D all 1.
So if you are porting MATLAB code that reads A off an arx model, read the
denominator with tfdata(sys, "v") instead. Importing such a line is refused
for exactly this reason rather than translated.
The same asymmetry reaches getpvec: an idpoly's parameter vector drops
B's leading zeros (they are the delay nk) and has no delay entry, while an
idtf's keeps the whole numerator and appends one. getpvec here is the
idtf reading.
What the identified classes carry that a console model does not#
| MATLAB carries | Here |
|---|---|
The noise polynomials C and D | Extra outputs on the estimator: [G, C] = armax(...), [G, C, D] = bj(...) |
| The parameter covariance | Nothing — getcov(sys) is always [] |
The Kalman gain K and the initial state x0 | Nothing — idssdata's fifth and sixth outputs are always zero; write the initial state on the simulation instead, initial(sys, x0) and lsim(sys, u, t, x0) |
An unspecified sample time (Ts = -1) | Nothing — write the sample time |
A process model (idproc) | Nothing — idproc is refused; write tf(K, [Tp1 1]) and set sys.InputDelay |
⚠ getcov being always empty is a real divergence, not a placeholder.
MATLAB answers [] for a model that was built rather than fitted, and a real
covariance matrix for one an estimator produced. No console model carries an
estimation report, so [] is the only true answer here — and a MATLAB script
that branches on isempty(getcov(m)) will take the other branch.
Things that surprise people#
These are MATLAB's behaviours, reproduced on purpose.
arx's first sample ismax(na, nb - (nk == 0), 1) + 1— a rule that does not mentionnk— and an input regressor reaching before the record contributes a zero rather than dropping its equation. The reading a careful implementer writes instead is a different estimate, and the two agree whenevernk = 0, so a case with no delay cannot tell them apart.ar's default approach is"fb", forward–backward — not least squares and not Burg — and"yw"silently uses the"ppw"window whatever you ask for.idinput's default type is"rbs", not"prbs", and only"prbs"is deterministic. For"prbs"the band's upper edge is a clock, not a filter: at0.5every register bit is held two samples.- A
prbsperiod that is not2^n - 1gives the first N samples of the next full sequence, so the signal is not periodic in the length you asked for. compare's initial state is estimated, not zero, andpredict/residhere use a zero initial state where MATLAB's default estimates one — so MATLAB's defaultresidanswers exactly 0 for its leading residuals where this answers a small number. Pass'InitialCondition','z'on MATLAB's side to compare like with like.compare's percentage andgoodnessOfFit's NRMSE run in opposite directions.goodnessOfFit(x, xref, "NRMSE")is a ratio where 0 is perfect;compare's fit is100 * (1 - that), where 100 is.- A bare
pem(u, y, Ts)is refused here and is not refused by MATLAB, which answers a default state-space estimate instead. This console has no order to invent. spa's default lag window ismin(30, floor(N/5))— measured, not thefloor(N/10)its source appears to state.n4sid'sA,B,Cand its initial state agree with MATLAB's only up to the SIGN of each state. The states come out of a singular value decomposition, and the sign of a singular vector is not fixed by the data: flipping state k negates row k ofB, column k ofCand row k of the Kalman gain together, which is a change of basis and leaves the model alone. So comparetfdata,poleordcgain— neverA— and read an initial state through the response it produces,initial(sys, x0).n4sid's sample time labels the model and does not scale the estimate. A subspace estimate is written in lags, son4sid(u, y, 0.1, 2)andn4sid(u, y, 1, 2)answer the same four matrices and differ only in theTsstamped on them. (MATLAB is the same:n4sid(u, y, 2, 'Ts', 0.1)and'Ts', 1agree to the last bit.) A sample time of zero is a different thing entirely — MATLAB's continuous-time subspace estimate — and is refused here rather than approximated.n4sidon a noise-free record is not reproducible, and that is a property of the algorithm. Its default weighting whitens the future outputs before decomposing them, so what it takes singular values of are the canonical correlations between future and past. With real noise those are distinct and the state subspace is well determined. On a noise-free record they all saturate at 1.0 to the last bit, there is no gap left to pick the subspace by, and two correct implementations answer different — both perfectly fitting — models. If you are checkingn4sidagainst MATLAB, check it on data that has noise in it.- A record too short for the order is refused here and is not refused by
MATLAB. MATLAB shrinks the horizons until the record can build them and
then goes on even when nothing is left — on a three-sample record at order
2 it answers a model with both poles at the origin, which is not an
estimate of anything. Give
n4sidroughly five times the order in samples, which is what it needs to answer something meaningful anyway. "DisturbanceModel", "none"is not a cosmetic flag. It changes three things at once: the automatic horizon comes from a different order search, it has no past output horizon at all, and the state matrix is reflected into the unit disc. Expect a different model, not the same one without a noise term.
What is not here#
procest, greyest, idgrey, nlarx and nlhw all answer objects with
methods, and iddata plots and the System Identification app are click-driven
tools. recursiveARX, recursiveLS and recursiveARMAX are System objects —
constructed, stepped and reset — and the estimators inside them are here as
plain functions instead: rarx(z, [na nb nk], adm, adg) for the first two and
rpem(z, [na nb nc 0 0 nk], adm, adg) for the third.
cra and impulseest are refused rather than approximated, because neither is
the correlation or the FIR fit it looks like: cra prewhitens the record with
an AR(10) model of the input before it correlates, and impulseest fits an AR
model to the input, filters the record by it, and caps the answer's length by
the input's order of persistent excitation. Both prefilters are part of the
estimate. Reach for covf(z, M), etfe(u, y, Ts) and spa(u, y, Ts) for the
nonparametric answers, and arx(u, y, Ts, [0 nb nk]) for the plain FIR fit.
ssest and ssregest are refused rather than approximated, and the refusal
says which algorithm is missing. ssest is n4sid followed by a
state-space prediction-error refinement, over an engine this console does not
have — and its default answer is a continuous-time model, which is a second
thing missing. ssregest estimates a regularised high-order ARX model and
reduces it, which is neither n4sid's algorithm nor arx's. Reach for
n4sid(u, y, Ts, nx), which is the subspace estimate ssest starts from.
tfest, and the two models one name answers#
tfest is MATLAB's, and it has the shape T6.1 gives every estimator here — the
samples first, the record's sample time third:
tfest(u, y, Ts, np) % nz defaults to np - 1
tfest(u, y, Ts, np, nz)
tfest(u, y, Ts, np, nz, ioDelay)
tfest(..., "Ts", Ts) % ... and now the answer is DISCRETE
tfest(..., "Feedthrough", true)
⚠ That third argument is the record's sample time and it does not make the
answer discrete. MATLAB's tfest answers a continuous-time model by
default, and the "Ts" name-value is the only thing that changes that. It is
MATLAB's own asymmetry with the rest of its own toolbox — arx(u, y, [2 2 1])
with no sample time assumes 1 and answers a discrete model — and it is worth
seeing once:
> tfest(un, yn, 1, 2, 1).Ts
0
> tfest(un, yn, 1, 2, 1, "Ts", 1).Ts
1
Same record, same orders, one name-value apart, two different models.
The discrete answer is oe#
Not "close to" oe. It is oe, under an order mapping:
tfest(u, y, Ts, np, nz, "Ts", Ts)isoe(u, y, Ts, [nz, np, 1]), andoe(u, y, Ts, [nz + 1, np, 0])whennzis 0 or"Feedthrough"is on. An io delay of k samples adds k to that last order.
Measured bit-identical over 18 order sets on two records, one of them with an
input delay of three, and over 36 more spanning io delays of 0 to 3 with
feedthrough on and off. MATLAB's own tfestUsingPoly.m is why: a discrete
tfest converts its model to an output-error idpoly whose first numerator
coefficient is fixed at zero — which is the same rule "Feedthrough" turns
off — and calls pem, the search oe already runs here. ident.itest asserts
the four identities rather than describing them, so the equivalence is checked
on every run.
The continuous answer is its own engine#
tfest(u, y, Ts, np, nz) with no "Ts" runs two things, both transcribed from
MATLAB rather than replaced with a fit of the console's choosing:
- The initialisation —
inival_time.min its default'iv'mode: agpmfstate-variable-filter fit, then refined instrumental variables (SRIVC). Both filter the record through continuous prototypes and solve a linear system, so both land on determined numbers. - The refinement —
ctTfestUsingSS.m. The model is mapped to a structured state space in controller canonical form and its prediction error is minimized bygnnsctandidminimizer, sampling the model at the record's sample time inside every evaluation. That is what makes this a continuous-time estimate of discrete-time data rather than a discrete fit converted afterwards.
⚠ The initial state is a free parameter here, and it changes the answer.
InitialCondition is 'auto', and for a single experiment over a structured
model that resolves to 'estimate' — so x0 joins the numerator and denominator
in the search, starting from a least-squares fit. Measured on R2026a: forcing a
zero initial state answers 0.61987147 s + 0.59417862 where the default
answers 0.61992538 s + 0.60400180 on the same record and orders. A fit that
starts from a zero state agrees to two digits and no further.
Over 42 order sets on three records, 38 agree with R2026a below 1e-9 relative. The four that do not all ran the 20-iteration cap with matching iteration and function counts on both sides — round-off accumulated over a hundred-odd evaluations of a fit that never converged, not a different search.
The rules that differ between the two arms#
| continuous | discrete | |
|---|---|---|
| asked for by | nothing (the default) | "Ts", Ts |
nz > np | an error, in MATLAB's words | legal |
"Feedthrough" | ignored (use nz < np) | frees the leading coefficient |
a nonzero ioDelay | refused here — see below | carried as leading numerator zeros |
A continuous answer has nowhere to keep a delay: a tf on this console has no
delay property, and MATLAB reports this one as m.IODelay beside the model
rather than inside it. So the console refuses it and names the call that
answers the same numbers — measured bit for bit over 36 order sets, an io delay
of k samples is exactly the estimate over [zeros(1, k), u(1:end-k)].
Two of MATLAB's warnings have no channel here and the answer is the same
either way: a "Ts" that disagrees with the record's is ignored ("the model's
sample time will be ignored"), and "Feedthrough" on a continuous fit is
ignored ("use NZ < NP to enforce absence of feedthrough"). MATLAB's second
output, [sys, ic] = tfest(...), is an initialCondition object and is off
this console for the reason every object is.
The console's own estimators are tffit#
⚠ tfest on this console meant something else until this row landed, and
if you have a script that calls it, this is the paragraph you want. The
console's own estimator family is now spelled:
tffit(u, y, numOrder, denOrder, Ts[, "method"]) % was tfest
tffitPercent(u, y, numOrder, denOrder, Ts[, ...]) % was tfestfit
tffitMethods % was tfestMethods
It moved because the collision was one of meaning rather than shape:
tffit reads arguments 3 and 4 as a numerator and a denominator order,
where MATLAB's tfest reads them as poles and zeros. Both five-argument
calls parsed, and both answered a model — a different one. No arity rule
separates them, so one of the two names had to move, and it was the one MATLAB
does not have.
tffit fits a discrete transfer function by one of ICore's ten estimators
(tffitMethods lists them with a summary of each), and tffitPercent reports
that fit's NRMSE percentage — the number the System Identification panel prints
beside the model. Neither is MATLAB's algorithm and neither has a MATLAB call to
cross to — MATLAB would have to run ICore's
estimator first. simtf(sys, u) simulates the result. If you want an answer
MATLAB reproduces, use tfest, oe or armax.