@@ -227,13 +227,15 @@ Calculate default frequency vector and put system in array of not already array.
227227for which `_default_freq_vector` is defined.
228228Check 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
276279end
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]
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
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"""
406423nyquistplot
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"""
734758marginplot
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
9911026end
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
0 commit comments