Skip to content

Commit 2062144

Browse files
authored
Merge pull request #993 from JuliaControl/adaptivegrid
use adaptive sampling for the frequency grid in bode and nyquist plots
2 parents 559f102 + ad2454c commit 2062144

File tree

3 files changed

+140
-25
lines changed

3 files changed

+140
-25
lines changed

lib/ControlSystemsBase/src/freqresp.jl

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -380,9 +380,14 @@ frequencies `w`. See also [`sigmaplot`](@ref)
380380
end
381381
@autovec (1) sigma(sys::LTISystem) = sigma(sys, _default_freq_vector(sys, Val{:sigma}()))
382382

383-
function _default_freq_vector(systems::Vector{<:LTISystem}, plot)
384-
min_pt_per_dec = 60
385-
min_pt_total = 200
383+
function _default_freq_vector(systems::Vector{<:LTISystem}, plot; adaptive=false)
384+
if adaptive
385+
min_pt_per_dec = 100
386+
min_pt_total = 1000
387+
else
388+
min_pt_per_dec = 60
389+
min_pt_total = 200
390+
end
386391
bounds = map(sys -> _bounds_and_features(sys, plot)[1], systems)
387392
w1 = minimum(minimum, bounds)
388393
w2 = maximum(maximum, bounds)
@@ -394,8 +399,8 @@ function _default_freq_vector(systems::Vector{<:LTISystem}, plot)
394399
end
395400
w
396401
end
397-
_default_freq_vector(sys::LTISystem, plot) = _default_freq_vector(
398-
[sys], plot)
402+
_default_freq_vector(sys::LTISystem, plot; kwargs...) = _default_freq_vector(
403+
[sys], plot; kwargs...)
399404

400405
function _bounds_and_features(sys::LTISystem, plot::Val)
401406
# Get zeros and poles for each channel

lib/ControlSystemsBase/src/plotting.jl

Lines changed: 128 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -227,13 +227,15 @@ Calculate default frequency vector and put system in array of not already array.
227227
for which `_default_freq_vector` is defined.
228228
Check that system dimensions are compatible.
229229
"""
230-
_processfreqplot(plottype, system::LTISystem, args...) =
231-
_processfreqplot(plottype, [system], args...)
230+
_processfreqplot(plottype, system::LTISystem, args...; kwargs...) =
231+
_processfreqplot(plottype, [system], args...; kwargs...)
232232
# Catch when system is not vector, with and without frequency input
233233

234234
# Catch correct form
235-
function _processfreqplot(plottype, systems::AbstractVector{<:LTISystem},
236-
w = _default_freq_vector(systems, plottype))
235+
_processfreqplot(plottype, systems::AbstractVector{<:LTISystem}; adaptive=false) =
236+
_processfreqplot(plottype, systems, _default_freq_vector(systems, plottype; adaptive))
237+
238+
function _processfreqplot(plottype, systems::AbstractVector{<:LTISystem}, w; kwargs...)
237239

238240
if !_same_io_dims(systems...)
239241
error("All systems must have the same input/output dimensions")
@@ -254,6 +256,7 @@ optionally provided. To change the Magnitude scale see [`setPlotScale`](@ref). T
254256
- If `hz=true`, the plot x-axis will be displayed in Hertz, the input frequency vector is still treated as rad/s.
255257
- `balance`: Call [`balance_statespace`](@ref) on the system before plotting.
256258
- `adjust_phase_start`: If true, the phase will be adjusted so that it starts at -90*intexcess degrees, where `intexcess` is the integrator excess of the system.
259+
- `adaptive`: If true, an adaptive frequency grid is used in order to keep the number of plotted points low, while resolving features in the frequency response well. If a manually provided frequency vector is used, this may be downsampled before plotting.
257260
258261
`kwargs` is sent as argument to RecipesBase.plot.
259262
"""
@@ -275,8 +278,10 @@ function _get_plotlabel(s, i, j)
275278
end
276279
end
277280

278-
@recipe function bodeplot(p::Bodeplot; plotphase=true, ylimsphase=(), unwrap=true, hz=false, balance=true, adjust_phase_start=true)
279-
systems, w = _processfreqplot(Val{:bode}(), p.args...)
281+
_span(vec) = -(reverse(extrema(vec))...)
282+
283+
@recipe function bodeplot(p::Bodeplot; plotphase=true, ylimsphase=(), unwrap=true, hz=false, balance=true, adjust_phase_start=true, adaptive=true)
284+
systems, w = _processfreqplot(Val{:bode}(), p.args...; adaptive)
280285
ws = (hz ? 1/(2π) : 1) .* w
281286
ny, nu = size(systems[1])
282287
s2i(i,j) = LinearIndices((nu,(plotphase ? 2 : 1)*ny))[j,i]
@@ -328,7 +333,13 @@ end
328333
label --> lab
329334
end
330335
group --> group_ind
331-
ws, magdata
336+
if adaptive
337+
lmag = _PlotScale == "dB" ? magdata : log.(magdata)
338+
wsi, _, inds = downsample(ws, lmag, _span(lmag)/300)
339+
wsi, magdata[inds]
340+
else
341+
ws, magdata
342+
end
332343
end
333344
plotphase || continue
334345

@@ -349,9 +360,15 @@ end
349360
xguide --> (hz ? "Frequency [Hz]" : "Frequency [rad/s]")
350361
label --> ""
351362
group --> group_ind
352-
ws, unwrap ? ControlSystemsBase.unwrap(phasedata.*(pi/180)).*(180/pi) : phasedata
353-
end
363+
phasedata = unwrap ? ControlSystemsBase.unwrap(phasedata.*(pi/180)).*(180/pi) : phasedata
364+
# NOTE: we should only downsample if the user hasn't provided w themselves
365+
if adaptive
366+
downsample(ws, phasedata, _span(phasedata)/300)[1:2]
367+
else
368+
ws, phasedata
369+
end
354370

371+
end
355372
end
356373
end
357374
end
@@ -404,8 +421,8 @@ optionally provided.
404421
`kwargs` is sent as argument to plot.
405422
"""
406423
nyquistplot
407-
@recipe function nyquistplot(p::Nyquistplot; Ms_circles=Float64[], Mt_circles=Float64[], unit_circle=false, hz=false, critical_point=-1, balance=true)
408-
systems, w = _processfreqplot(Val{:nyquist}(), p.args...)
424+
@recipe function nyquistplot(p::Nyquistplot; Ms_circles=Float64[], Mt_circles=Float64[], unit_circle=false, hz=false, critical_point=-1, balance=true, adaptive=true)
425+
systems, w = _processfreqplot(Val{:nyquist}(), p.args...; adaptive)
409426
ny, nu = size(systems[1])
410427
nw = length(w)
411428
layout --> (ny,nu)
@@ -432,7 +449,14 @@ nyquistplot
432449
label --> lab
433450
end
434451
hover --> [hz ? Printf.@sprintf("f = %.3g", w/2π) : Printf.@sprintf("ω = %.3g", w) for w in w]
435-
(redata, imdata)
452+
if adaptive
453+
indsre = downsample(w, redata, 1/500)[3]
454+
indsim = downsample(w, imdata, 1/500)[3]
455+
inds = sort!(union(indsre, indsim))
456+
redata[inds], imdata[inds]
457+
else
458+
redata, imdata
459+
end
436460
end
437461

438462
if si == length(systems)
@@ -732,8 +756,8 @@ Plot all the amplitude and phase margins of the system(s) `sys`.
732756
`kwargs` is sent as argument to RecipesBase.plot.
733757
"""
734758
marginplot
735-
@recipe function marginplot(p::Marginplot; plotphase=true, hz=false, balance=true, adjust_phase_start=true)
736-
systems, w = _processfreqplot(Val{:bode}(), p.args...)
759+
@recipe function marginplot(p::Marginplot; plotphase=true, hz=false, balance=true, adjust_phase_start=true, adaptive=true)
760+
systems, w = _processfreqplot(Val{:bode}(), p.args...; adaptive)
737761
ny, nu = size(systems[1])
738762
s2i(i,j) = LinearIndices((nu,(plotphase ? 2 : 1)*ny))[j,i]
739763
layout --> ((plotphase ? 2 : 1)*ny, nu)
@@ -790,7 +814,14 @@ marginplot
790814
end
791815
primary := true
792816
seriestype := :bodemag
793-
w, bmag[i, j, :]
817+
m = bmag[i, j, :]
818+
if adaptive
819+
lmag = _PlotScale == "dB" ? m : log.(m)
820+
wsi, _, inds = downsample(w, lmag, _span(lmag)/300)
821+
wsi, m[inds]
822+
else
823+
w, m
824+
end
794825
end
795826

796827
#Plot gain margins
@@ -818,7 +849,11 @@ marginplot
818849
@series begin
819850
primary := true
820851
seriestype := :bodephase
821-
w, phasedata
852+
if adaptive
853+
downsample(w, phasedata, _span(phasedata)/300)[1:2]
854+
else
855+
w, phasedata
856+
end
822857
end
823858
@series begin
824859
primary := false
@@ -917,7 +952,7 @@ Gang-of-Four plot.
917952
`sigma` determines whether a [`sigmaplot`](@ref) is used instead of a [`bodeplot`](@ref) for MIMO `S` and `T`.
918953
`kwargs` are sent as argument to RecipesBase.plot.
919954
"""
920-
function gangoffourplot(P::Union{<:Vector, LTISystem}, C::Vector, args...; minimal=true, Ms_lines = [1.0, 1.25, 1.5], Mt_lines = [], sigma = true, plotphase=false, kwargs...)
955+
function gangoffourplot(P::Union{<:Vector, LTISystem}, C::Vector, args...; minimal=true, Ms_lines = [1.0, 1.25, 1.5], Mt_lines = [], sigma = true, plotphase=false, adaptive=true, kwargs...)
921956
if P isa LTISystem # Don't broadcast over scalar (with size?)
922957
P = [P]
923958
end
@@ -927,16 +962,16 @@ function gangoffourplot(P::Union{<:Vector, LTISystem}, C::Vector, args...; minim
927962

928963
gofs = gangoffour.(P,C)
929964
S,D,N,T = ntuple(i->getindex.(gofs, i), 4)
930-
bp = (args...; kwargs...) -> sigma ? sigmaplot(args...; kwargs...) : bodeplot(args...; plotphase=false, kwargs...)
965+
bp = (args...; kwargs...) -> sigma ? sigmaplot(args...; kwargs...) : bodeplot(args...; plotphase=false, adaptive, kwargs...)
931966
f1 = bp(S, args...; show=false, title="S = 1/(1+PC)", kwargs...)
932967
if !isnothing(Ms_lines) && !isempty(Ms_lines)
933968
Plots.hline!(Ms_lines', l=(:dash, [:green :orange :red :darkred :purple]), sp=1, primary=false, lab=string.(Ms_lines'), ylims=(1e-2,4))
934969
else
935970
Plots.hline!([1.0], l=(:dash, :black), sp=1, ylims=(1e-2,1.8))
936971
end
937-
f2 = bodeplot(D, args...; show=false, title="P/(1+PC)", plotphase=false, kwargs...)
972+
f2 = bodeplot(D, args...; show=false, title="P/(1+PC)", plotphase=false, adaptive, kwargs...)
938973
Plots.hline!(ones(1, ninputs(D[1])*noutputs(D[1])), l=(:black, :dash), primary=false)
939-
f3 = bodeplot(N, args...; show=false, title="C/(1+PC)", plotphase=false, kwargs...)
974+
f3 = bodeplot(N, args...; show=false, title="C/(1+PC)", plotphase=false, adaptive, kwargs...)
940975
f4 = bp(T, args...; show=false, title="T = PC/(1+PC)", ylims=(1e-2,4), kwargs...)
941976
if !isnothing(Mt_lines) && !isempty(Mt_lines)
942977
Plots.hline!(Mt_lines', l=(:dash, [:green :orange :red :darkred :purple]), primary=false, lab=string.(Mt_lines'), ylims=(1e-2,4))
@@ -989,3 +1024,76 @@ rgaplot
9891024
end
9901025
end
9911026
end
1027+
1028+
1029+
## Adaptive sampling
1030+
# Code adapted from https://github.com/iuliancioarca/AdaptiveSampling.jl/blob/master/LICENSE
1031+
1032+
function downsample(t,y,detail_th)
1033+
# Compress signal by removing redundant points.
1034+
# Adjust waveform detail/compression ratio with 'detail_th' (maximum allowed
1035+
# difference between original and approximated points from the signal)
1036+
yln = length(y)
1037+
idx_l = 1
1038+
idx_r = yln
1039+
idx_d_max = 1
1040+
cond_break = true
1041+
d_max = 0.0
1042+
M = zeros(Int,yln) # hash table for relevant indices
1043+
idx2save = zeros(Int,yln+2)
1044+
cnt = 2
1045+
idx2save[1:2] = [1,yln]
1046+
while cond_break
1047+
# get maximum error(difference) and index, between original chunk of signal
1048+
# and linear approximation
1049+
d_max, idx_d_max = get_d_max(idx_l,idx_r,y)
1050+
# save all indices
1051+
M[idx_d_max] = idx_r
1052+
if d_max > detail_th
1053+
# if computed error is greater than maximum allowed error, save
1054+
# next point index and call get_d_max(idx_l,idx_r,y) at next
1055+
# iteration; keep going towards leftmost branches
1056+
cnt = cnt + 1
1057+
idx_r = idx_d_max
1058+
idx2save[cnt] = idx_d_max
1059+
else
1060+
# if computed error is smaller than maximum allowed error, stop, go
1061+
# right(to the next waveform segment) and call get_d_max(idx_l,idx_r,y)
1062+
# at the next iteration
1063+
idx_l = idx_r;
1064+
if idx_l != yln
1065+
idx_r = M[idx_l]
1066+
else
1067+
cond_break = false
1068+
end
1069+
end
1070+
end
1071+
# sort all indexes corresponding to relevent points and generate resampled
1072+
# signal
1073+
idx2save = idx2save[1:cnt]
1074+
idx2save = sort(idx2save)
1075+
t_new = @view t[idx2save]
1076+
y_new = @view y[idx2save]
1077+
return t_new, y_new, idx2save
1078+
end
1079+
function get_d_max(idx_l,idx_r,y)
1080+
# cut segment to be resampled
1081+
yp = view(y,idx_l:idx_r)
1082+
# construct linear approximation
1083+
dr = LinRange(y[idx_l], y[idx_r], length(yp))
1084+
# compute distance(error) and get index of maximum error
1085+
# -> this will be used for further splitting the
1086+
# signal and will be part of the final resampled signal
1087+
d_max = 0.0
1088+
idx_d_max = 1
1089+
err_val = 0.0
1090+
for i = 1:length(yp)
1091+
err_val = abs(yp[i] - dr[i])
1092+
if err_val > d_max
1093+
d_max = err_val
1094+
idx_d_max = i
1095+
end
1096+
end
1097+
idx_d_max = idx_d_max + idx_l - 1
1098+
return d_max, idx_d_max
1099+
end

lib/ControlSystemsBase/test/test_plots.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ end
5555

5656
sys = ssrand(3,3,3)
5757
sigmaplot(sys, extrema=true)
58+
bodeplot(sys, adaptive=false)
59+
nyquistplot(sys, adaptive=false)
5860

5961
Gmimo = ssrand(2,2,2,Ts=1)
6062
@test_nowarn plot(step(Gmimo, 10), plotx=true)

0 commit comments

Comments
 (0)