Skip to content

Commit db53ded

Browse files
authored
add support for multiple coherence (#194)
* add support for multiple coherence * tweak tol
1 parent 8b7f06f commit db53ded

4 files changed

Lines changed: 133 additions & 4 deletions

File tree

src/frd.jl

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,9 @@ from which it is obvious that ``0 ≤ κ² ≤ 1`` and that κ² is close to 1 i
260260
"""
261261
function coherence(d::AbstractIdData; n = length(d) ÷ 10, noverlap = n ÷ 2, window = hamming, method=:welch, σ = 0.05)
262262
noutputs(d) == 1 || throw(ArgumentError("coherence only supports a single output. Index the data object like `d[i,j]` to obtain the `i`:th output and the `j`:th input."))
263-
ninputs(d) == 1 || throw(ArgumentError("coherence only supports a single input. Index the data object like `d[i,j]` to obtain the `i`:th output and the `j`:th input."))
263+
if ninputs(d) > 1
264+
return multiple_coherence(d; n, noverlap, window, σ)
265+
end
264266
y, u, h = vec(output(d)), vec(input(d)), sampletime(d)
265267
if method === :welch
266268
Syy, Suu, Syu = wcfft(y, u, n = n, noverlap = noverlap, window = window)
@@ -291,6 +293,80 @@ function wcfft(y, u; n = length(y) ÷ 10, noverlap = n ÷ 2, window = hamming)
291293
Syy, Suu, Syu
292294
end
293295

296+
function multiple_coherence(d::AbstractIdData; n = length(d) ÷ 10, noverlap = n ÷ 2, window = hamming, σ = 0.05)
297+
noutputs(d) == 1 || throw(ArgumentError("multiple_coherence only supports a single output."))
298+
ninputs(d) > 1 || throw(ArgumentError("multiple_coherence requires multiple inputs. Use `coherence` for single input."))
299+
300+
y = vec(output(d))
301+
u = time1(input(d))
302+
h = sampletime(d)
303+
304+
Syy, Suu, Syu = wcfft_mimo(y, u, n = n, noverlap = noverlap, window = window)
305+
306+
n_freqs = length(Syy)
307+
γ2 = zeros(n_freqs)
308+
309+
for i in 1:n_freqs
310+
s_xy = conj(Syu[i])
311+
num = real(dot(s_xy, Suu[i] \ s_xy))
312+
γ2[i] = num / Syy[i]
313+
end
314+
315+
γ2 = clamp.(γ2, 0.0, 1.0)
316+
317+
Sch = FRD(freqvec(h, γ2), γ2)
318+
return Sch
319+
end
320+
321+
function wcfft_mimo(y::AbstractVector, u::AbstractMatrix; n = length(y) ÷ 10, noverlap = n ÷ 2, window = hamming)
322+
n_samples, n_inputs = size(u)
323+
n_samples == length(y) || throw(DimensionMismatch("Input and output lengths differ"))
324+
325+
win, norm2 = DSP.Periodograms.compute_window(window, n)
326+
327+
# Dimensions for FFT
328+
nfft = nextfastfft(n)
329+
n_freqs = nfft ÷ 2 + 1
330+
331+
# Initialize Accumulators
332+
Syy = zeros(Float64, n_freqs)
333+
Suu = [zeros(ComplexF64, n_inputs, n_inputs) for _ in 1:n_freqs]
334+
Syu = [zeros(ComplexF64, n_inputs) for _ in 1:n_freqs] # Vector of vectors
335+
336+
# Calculate window stride
337+
step = n - noverlap
338+
339+
# Segment Loop (Manual splitting to handle Matrix u safely)
340+
for i in 1:step:(n_samples - n + 1)
341+
# Extract segments and apply window
342+
y_seg = y[i:i+n-1] .* win
343+
u_seg = u[i:i+n-1, :] .* win # Broadcast window across columns
344+
345+
# Compute FFTs
346+
Y_f = rfft(y_seg)
347+
U_f = rfft(u_seg) # FFT along time dimension (dim 1)
348+
349+
# Accumulate Spectra per frequency bin
350+
for k in 1:n_freqs
351+
Y_k = Y_f[k] # Scalar
352+
U_k = U_f[k, :] # Vector (1 x Inputs)
353+
354+
# Auto-spectrum Output (Scalar)
355+
Syy[k] += abs2(Y_k)
356+
357+
# Auto-spectral Matrix Input (Matrix: Inputs x Inputs)
358+
# Outer product: U * U'
359+
Suu[k] .+= U_k * U_k'
360+
361+
# Cross-spectral Vector (Vector: Inputs)
362+
# We compute S_yx = Y * U' (Output * conj(Input))
363+
Syu[k] .+= Y_k .* conj.(U_k)
364+
end
365+
end
366+
367+
return Syy, Suu, Syu
368+
end
369+
294370

295371

296372
"""

src/plotting.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,6 @@ coherenceplot
268268
ArgumentError("Call like this: coherenceplot(iddata; hz=false)")
269269
d = p.args[1]
270270
d isa AbstractIdData || throw(ae)
271-
ninputs(d) == 1 || throw(ArgumentError("coherenceplot only supports a single input. Index the data object like `d[i,j]` to obtain the `i`:th output and the `j`:th input."))
272271
if length(p.args) >= 2
273272
kwargs = p.args[2]
274273
else
@@ -282,7 +281,7 @@ coherenceplot
282281
title --> "Coherence"
283282
label --> false
284283
for i = 1:d.ny
285-
frd = coherence(d[i,1]; kwargs...)
284+
frd = coherence(d[i,:]; kwargs...)
286285
@series begin
287286
inds = findall(x -> x == 0, frd.w)
288287
useinds = setdiff(1:length(frd.w), inds)

test/test_frd.jl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,31 @@ plot!(G2, subplot = 1, lab = "G Est W", alpha = 0.3, title = "Process model")
5858
plot!(N2, subplot = 2, lab = "N Est W", alpha = 0.3, title = "Noise model")
5959

6060

61+
62+
@testset "multiple coherence" begin
63+
@info "Testing multiple coherence"
64+
nu = 2
65+
nx = 2
66+
ny = 1
67+
N = 10000
68+
u = randn(nu, N)
69+
sys = ssrand(ny, nu, nx, Ts=1, proper=true)
70+
sys.B .= I(nu)
71+
y, t, x = lsim(sys, u)
72+
y .+= sin.(1 .* t')
73+
d = iddata(y, u, 1)
74+
c = coherence(d)
75+
plot(c, yscale=:identity)
76+
coherenceplot(d)
77+
78+
@test mean(c.r) > 0.99
79+
using ControlSystemIdentification: rad
80+
@test mean(c[0.999rad:1.001rad].r) < 0.2 # low coherence at disturbance frequency
81+
end
82+
83+
84+
85+
6186
for op in (+, -, *, /)
6287
@test op(G, G) isa FRD
6388
end

test/test_subspace.jl

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,4 +401,33 @@ using Statistics
401401

402402
@test meanmodel era(ds, 2, 50, 50, round(Int, 10/Ts), p=1)
403403
@test meanmodel era(ds, 2; m=50, n=50, l=round(Int, 10/Ts), p=1)
404-
end
404+
end
405+
406+
## What happens when there is a constant input offset?
407+
@testset "constant bias input" begin
408+
@info "Testing constant bias input"
409+
nu = 1
410+
nx = 2
411+
ny = 1
412+
off = 10
413+
N = 1000
414+
u = [randn(nu, N); ones(1, N)*off]
415+
sys = ssrand(ny, nu, nx, Ts=1, proper=true)
416+
sys = ss(sys.A, sys.B .* [1 1], sys.C, 0, sys.Ts)
417+
y, t, x = lsim(sys, u)
418+
d = iddata(y, u[1:1, :], 1)
419+
sys_id = subspaceid(d, nx+1)
420+
421+
# @test hinfnorm(sys[1,1] - sys_id.sys)[1] < 1e-10
422+
@test observability(sys_id, atol=1e-6).isobservable
423+
@test !controllability(sys_id, atol=1e-6).iscontrollable
424+
425+
# bodeplot([sys[1,1], sys_id.sys], exp10.(LinRange(-3, log10(pi), 100)), plotphase=false)
426+
427+
#=
428+
With a constant input offset, subspace id can compensate exactly as long as the state dimension is increased by one.
429+
Modal form of the estimated system gets one pole in 1, and the B and C matrix entries corresponding to this state dimension are zero.
430+
431+
The disturbance state is observable but not controllable
432+
=#
433+
end

0 commit comments

Comments
 (0)