In this example, we will estimate a model for a flexible robot arm.

We will get the data from STADIUS's Identification Database

using DelimitedFiles, Plots
using ControlSystemIdentification, ControlSystemsBase

url = "https://ftp.esat.kuleuven.be/pub/SISTA/data/mechanical/robot_arm.dat.gz"
zipfilename = "/tmp/flex.dat.gz"
path = Base.download(url, zipfilename)
run(`gunzip -f $path`)
data = readdlm(path[1:end-3])
u = data[:, 1]' # torque
y = data[:, 2]' # acceleration
d = iddata(y, u, 0.01) # sample time not specified for data, 0.01 is a guess
InputOutput data of length 1024, 1 outputs, 1 inputs, Ts = 0.01

The input consists of the motor torque and the output is the acceleration of the arm.

Before we estimate any model, we inspect the data and the coherence function

xt = [2,5,10,20,50]
plot(
    plot(d),
    coherenceplot(d, xticks=(xt,xt), hz=true),
)
Example block output

The coherence is low for high frequencies as well as frequencies between 2 and 6 Hz. We should thus be careful with relying on the estimated model too much in these frequency ranges. The reason for the low coherence may be either a poor signal-to-noise ratio, or the presence of nonlinearities. For systems with anti-resonances, like this one, the SNR is often poor at the notch frequencies (indeed, a notch frequency is defined as a frequency where there will be very little signal). We can investigate the spectra of the input and output using welchplot (see also specplot)

welchplot(d, yticks=(xt,xt))
Example block output

Not surprisingly, we see that the input has very little power above 20Hz, this is the reason for the low coherence above 20Hz. Limiting the bandwidth of the excitation signal is usually a good thing, mechanical structures often exhibit higher-order resonances and nonlinear behavior at high frequencies, which we see eveidence of in the output spectrum at around 34Hz.

We also split the data in half, and use the first half for estimation and the second for validation. We'll use a subspace-based identification algorithm for estimation

dtrain = d[1:end÷2]
dval = d[end÷2:end]

# A model of order 4 is reasonable, a double-mass model. We estimate two models, one using subspace-based identification and one using the prediction-error method
model_ss = subspaceid(dtrain, 4, focus=:prediction)
model_pem, _ = newpem(dtrain, 4; sys0 = model_ss, focus=:prediction)

predplot(model_ss, dval, h=1)
predplot!(model_ss, dval, h=5, ploty=false)
simplot!(model_ss, dval, ploty=false)
Example block output

The figures above show the result of predicting $h={1, 10, \infty}$ steps into the future.

We can visualize the estimated models in the frequency domain as well. We show both the model estimated using PEM and a nonparametric estimate using a Fourier-based method (tfest), this method estimates a noise model as well.

w = exp10.(LinRange(-1, log10(pi/d.Ts), 200))
bodeplot(model_pem.sys, w, lab="PEM", plotphase=false, hz=true)
bodeplot!(model_ss.sys, w, lab="Subspace", plotphase=false, hz=true)
plot!(tfest(d), legend=:bottomleft, hz=true, xticks=(xt,xt))
Example block output

It looks like the model fails to capture the notches accurately. Estimating zeros is known to be hard, both in practice and in theory. In the estimated disturbance (labeled Noise), we see a peak at around 34Hz. This is likely an overtone due to nonlinearities.

We can also investigate how well the models predict for various prediction horizons, and compare that to how well the model does in open loop (simulation)

using Statistics
hs = [1:40; 45:5:80]
perrs_pem = map(hs) do h
    yh = predict(model_pem, d; h)
    ControlSystemIdentification.rms(d.y - yh) |> mean
end
perrs_ss = map(hs) do h
    yh = predict(model_ss, d; h)
    ControlSystemIdentification.rms(d.y - yh) |> mean
end
serr_pem = ControlSystemIdentification.rms(d.y - simulate(model_pem, d)) |> mean
serr_ss = ControlSystemIdentification.rms(d.y - simulate(model_ss, d)) |> mean

plot(hs, perrs_pem, lab="Prediction errors PEM", xlabel="Prediction Horizon", ylabel="RMS error")
plot!(hs, perrs_ss, lab="Prediction errors Subspace")
hline!([serr_pem], lab="Simulation error PEM", l=:dash, c=1, ylims=(0, Inf))
hline!([serr_ss], lab="Simulation error Subspace", l=:dash, c=2, legend=:bottomright, ylims=(0, Inf))
Example block output

We see that the prediction-error model does slightly better at prediction few-step predictions (indeed, this is what PEM optimizes), while the model identified using subspaceid does better in open loop. The simulation performance can be improved upon further by asking for focus=:prediction when the models are estimated.