using LinearAlgebra, Statistics
# Structure and sensors
L, b, h = 1., 0.03, 0.01
beam = Beam(L, b*h, b*h^3/12, 2.1e11, 7850.)
ξ = 0.01
x_sensors = [0.2, 0.35, 0.5, 0.7]
fs = 4096
ωn, kn = modefreq(beam, fs/2)
Φ_s = modeshape(beam, kn, x_sensors)
Φ_e = modeshape(beam, kn, [0.35])
nmodes = length(ωn)
# Impact response
t = 0.:1/fs:0.5
F = reshape(excitation(Hammer(100., 0., 9.7, 1e-4), t), 1, :)
prob = ForcedModalTimeProblem(ωn, Φ_s, ξ, Φ_e'*F, (zeros(nmodes), zeros(nmodes)), t)
sol = solve(prob)
x_true, v_true = sol.u, sol.du # Noise-free displacement and velocity
# Measurement: 20 dB SNR on each channel
SNR_true = 20.
y = agwn(x_true, SNR_true)Noise estimation and denoising
- You will learn how to estimate the noise level of a measured signal without knowing the noise-free signal, and how to choose a denoising method.
- Prerequisites: the Getting started page.
- Duration: about 15 minutes.
Many post-processing operations are very sensitive to noise: numerical differentiation, inverse problems (force identification, model updating, …), or Kalman filtering, whose tuning requires the noise covariance. Knowing the noise level of a signal, and being able to reduce it, is therefore an essential step. As usual, we work with synthetic data so that every result can be compared with the truth.
1 Step 1 – A noisy measurement
A simply supported beam is hit by an impact hammer at \(x\) = 0.35 m, and its displacement is recorded at four points during 0.5 s.
fig = Figure()
ax = Axis(fig[1, 1], xlabel = "Time (s)", ylabel = "Displacement (m)", title = "Sensor at x = 0.2 m")
lines!(ax, t, y[1, :], color = (:gray, 0.6), label = "Measured")
lines!(ax, t, x_true[1, :], label = "Noise-free")
xlims!(ax, 0., 0.5)
axislegend(ax, position = :rt)
fig2 Step 2 – Why noise matters: numerical differentiation
Suppose we need the velocity, but only the displacement was measured. Differentiating the measured signal is the obvious solution.
rel_error(a, ref) = [norm(a[i, :] - ref[i, :])/norm(ref[i, :]) for i in axes(ref, 1)]
v_noisy = gradient(y, t) # Differentiation along time (dims = 1: one signal per row)println("Displacement error (measured): ", round.(100rel_error(y, x_true), digits = 1), " %")
println("Velocity error (differentiated): ", round.(100rel_error(v_noisy, v_true), digits = 1), " %")Displacement error (measured): [10.0, 10.1, 10.2, 9.8] %
Velocity error (differentiated): [260.3, 294.8, 334.3, 277.3] %
A 10 % error on the displacement becomes an error of several hundred percent on the velocity. Differentiation amplifies the high-frequency content of the signal, where the noise dominates. This is typical of an ill-posed operation, and the reason why noise must be handled before such operations.
3 Step 3 – Estimating the noise level
In practice, the noise-free signal is unknown, and so is the noise level. varest estimates the noise variance of each channel from the measured signal only. Several methods are available:
BayesianEst()(default): a Bayesian version of the regularized smoothing.DerricoEst(): a fast statistical estimator, based on finite differences of the signal;GCVEst()andLCurveEst(): estimators based on the regularized smoothing of the signal, whose regularization parameter is selected by Generalized Cross-Validation or by the L-curve criterion;
methods = [("Bayesian", BayesianEst()), ("D'Errico", DerricoEst()), ("GCV", GCVEst()), ("L-curve", LCurveEst())]
SNR_est = [(name, estimated_SNR(y, varest(y, m))) for (name, m) in methods]nch = length(SNR_est[1][2])
data = vcat(hcat("True", fill(SNR_true, 1, nch)),
[hcat(name, round.(snr, digits = 1)') for (name, snr) in SNR_est]...)
pretty_table(
HTML,
data,
column_labels = ["Method"; ["Channel $i (dB)" for i in 1:nch]],
alignment = :c
)| Method | Channel 1 (dB) | Channel 2 (dB) | Channel 3 (dB) | Channel 4 (dB) |
|---|---|---|---|---|
| True | 20.0 | 20.0 | 20.0 | 20.0 |
| Bayesian | 20.1 | 20.0 | 20.0 | 20.2 |
| D'Errico | 20.5 | 19.3 | 19.9 | 20.0 |
| GCV | 20.5 | 20.3 | 20.2 | 20.6 |
| L-curve | 13.4 | 16.6 | 20.0 | 15.4 |
Bayesian, D’Errico and GCV estimators recover the SNR within about 1 dB. The L-curve estimator is less reliable here: it tends to over-smooth the signal, and thus to overestimate the noise level.
4 Step 4 – Denoising
The same ideas lead to denoising methods. Regularization-based methods solve \[
\widehat{\mathbf{x}} = \underset{\mathbf{x}}{\text{argmin}} \Vert \mathbf{y} - \mathbf{x} \Vert_2^2 + \lambda \Vert \mathbf{D} \mathbf{x} \Vert_2^2,
\] where \(\mathbf{D}\) is a second-order difference operator that penalizes non-smooth signals and \(\lambda\) is selected automatically. Kalman-based methods model the signal as the output of a random-walk state-space model, the noise variance being estimated with varest. The rts = true option adds a backward pass (Rauch-Tung-Striebel smoother), which uses the whole signal to estimate each sample.
denoisers = [("GCV", GCVDenoising()), ("L-curve", LCurveDenoising()), ("Bayesian", BayesDenoising()),
("Kalman filter", KalmanDenoising()), ("Kalman smoother", KalmanDenoising(rts = true))]
x_dn = Dict(name => denoising(y, alg) for (name, alg) in denoisers)nch = size(x_true, 1)
data = vcat(hcat("Measured signal", round.(100rel_error(y, x_true), digits = 1)'),
[hcat(name, round.(100rel_error(x_dn[name], x_true), digits = 1)') for (name, _) in denoisers]...)
pretty_table(
HTML,
data,
column_labels = ["Signal"; ["Channel $i error (%)" for i in 1:nch]],
alignment = :c
)| Signal | Channel 1 error (%) | Channel 2 error (%) | Channel 3 error (%) | Channel 4 error (%) |
|---|---|---|---|---|
| Measured signal | 10.0 | 10.1 | 10.2 | 9.8 |
| GCV | 3.5 | 3.1 | 2.3 | 2.9 |
| L-curve | 19.4 | 11.0 | 2.5 | 14.3 |
| Bayesian | 3.5 | 3.3 | 2.8 | 3.0 |
| Kalman filter | 7.5 | 7.2 | 6.7 | 6.9 |
| Kalman smoother | 5.6 | 4.6 | 4.4 | 4.7 |
fig = Figure()
ax = Axis(fig[1, 1], xlabel = "Time (s)", ylabel = "Displacement (m)", title = "Sensor at x = 0.2 m – zoom")
lines!(ax, t, y[1, :], color = (:gray, 0.4), label = "Measured")
lines!(ax, t, x_true[1, :], color = :black, label = "Noise-free")
lines!(ax, t, x_dn["GCV"][1, :], linestyle = :dash, label = "GCV")
lines!(ax, t, x_dn["Kalman smoother"][1, :], linestyle = :dot, label = "Kalman smoother")
xlims!(ax, 0., 0.1)
axislegend(ax, position = :rt)
figGCV and Bayesian regularization divide the error by about three, the Kalman smoother by about two. The Kalman filter alone, which only uses past samples, is less accurate, but it can be applied in real time. As for noise estimation, the L-curve criterion over-smooths some channels.
5 Step 5 – Back to differentiation
v_dn = gradient(x_dn["GCV"], t)
println("Velocity error from measured signal: ", round.(100rel_error(v_noisy, v_true), digits = 1), " %")
println("Velocity error from denoised signal: ", round.(100rel_error(v_dn, v_true), digits = 1), " %")Velocity error from measured signal: [260.3, 294.8, 334.3, 277.3] %
Velocity error from denoised signal: [21.0, 24.1, 13.1, 19.6] %
fig = Figure()
ax = Axis(fig[1, 1], xlabel = "Time (s)", ylabel = "Velocity (m/s)", title = "Sensor at x = 0.2 m")
lines!(ax, t, v_noisy[1, :], color = (:gray, 0.4), label = "From measured signal")
lines!(ax, t, v_true[1, :], color = :black, label = "Exact")
lines!(ax, t, v_dn[1, :], linestyle = :dash, label = "From denoised signal")
xlims!(ax, 0., 0.1)
axislegend(ax, position = :rt)
figDenoising before differentiating reduces the velocity error by more than an order of magnitude.
6 Summary
| Need | Recommended tool |
|---|---|
| Quick estimate of the noise level | varest(y) (D’Errico) |
| More accurate estimate | varest(y, GCVEst()) or varest(y, BayesianEst()) |
| Offline denoising | denoising(y, GCVDenoising()) or denoising(y, BayesDenoising()) |
| Real-time or streaming denoising | denoising(y, KalmanDenoising()) |
| Offline denoising of long signals | denoising(y, KalmanDenoising(rts = true)) |
The SNR of a signal is given by estimated_SNR(y, varest(y)), which is a useful sanity check of any measurement.
Reference pages: Noise estimation and denoising, Noise models.