Skip to content

Commit 2e3a591

Browse files
authored
fix gain margin crossing at zero freq (#1046)
* fix gain margin crossing at zero freq * handle edge case * more filtering
1 parent ea308d4 commit 2e3a591

2 files changed

Lines changed: 52 additions & 12 deletions

File tree

lib/ControlSystemsBase/src/analysis.jl

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ end
492492
"""
493493
wgm, gm, wpm, pm = margin(sys::LTISystem, w::Vector; full=false, allMargins=false, adjust_phase_start=true)
494494
495-
returns frequencies for gain margins, gain margins, frequencies for phase margins, phase margins
495+
returns frequencies for gain margins, gain margins (magnitude), frequencies for phase margins, phase margins (degrees)
496496
497497
- If `!allMargins`, return only the smallest margin
498498
- If `full` return also `fullPhase`
@@ -542,8 +542,19 @@ function sisomargin(sys::LTISystem, w::AbstractVector{<:Real}; full=false, allMa
542542
mag, phase, w = bode(sys, w)
543543
wgm, = _allPhaseCrossings(w, phase)
544544
gm = similar(wgm)
545+
remove = Int[]
545546
for i = eachindex(wgm)
546-
gm[i] = 1 ./ abs(freqresp(sys,wgm[i])[1])
547+
Giw = freqresp(sys,wgm[i])[1]
548+
if sign(w[1]) != sign(w[end]) && abs(Giw) > 1e6 && wgm[i] < 0.001
549+
# This tries to filter out extremely large gain margins that can arise when the Nyquist contour crosses the negative real axis at -Inf.
550+
# This is filter is in addition to the filter_th check in _findCrossings
551+
push!(remove, i)
552+
end
553+
gm[i] = 1 ./ abs(Giw)
554+
end
555+
if !isempty(remove)
556+
deleteat!(gm, remove)
557+
deleteat!(wgm, remove)
547558
end
548559
wpm, fi = _allGainCrossings(w, mag)
549560
pm = similar(wpm)
@@ -578,9 +589,10 @@ function sisomargin(sys::LTISystem, w::AbstractVector{<:Real}; full=false, allMa
578589
if adjust_phase_start && isrational(sys)
579590
intexcess, p, z, tol = integrator_excess_with_tol(sys)
580591
n_unstable_poles = count(real(p) > tol for p in p)
581-
if intexcess != 0
592+
first_positive_freq_ind = findfirst(>(0), w)
593+
if intexcess != 0 && (first_positive_freq_ind !== nothing)
582594
# Snap phase so that it starts at -90*intexcess
583-
nineties = round(Int, phase[1] / 90)
595+
nineties = round(Int, phase[first_positive_freq_ind] / 90)
584596
adjust = ((90*(-intexcess-nineties)) ÷ 360) * 360
585597
pm_unstable_adjust = n_unstable_poles*360 # count the number of unstable poles, and remove 360 for each. Be careful with poles that are counted as integrators
586598
pm = pm .+ adjust .- pm_unstable_adjust
@@ -595,9 +607,11 @@ function sisomargin(sys::LTISystem, w::AbstractVector{<:Real}; full=false, allMa
595607
end
596608
end
597609
margin(system::LTISystem; kwargs...) =
598-
margin(system, _default_freq_vector(system, Val{:bode}()); kwargs...)
610+
margin(system, _add_eps_negative(_default_freq_vector(system, Val{:bode}())); kwargs...)
599611
#margin(sys::LTISystem, args...) = margin(LTISystem[sys], args...)
600612

613+
_add_eps_negative(w) = [-(eps(eltype(w))); w] # The size of the negative frequency is a tradeoff, too small and the check `if abs(d) > 20` in _findCrossings may fail, but too large and we get an inaccurate interpolation.
614+
601615
# Interpolate the values in "list" given the floating point "index" fi
602616
function interpolate(fi, list)
603617
fif = floor.(Integer, fi)
@@ -613,19 +627,25 @@ function _allPhaseCrossings(w, phase)
613627
#Calculate numer of times real axis is crossed on negative side
614628
n = fld.(phase.+180,360) #Nbr of crossed
615629
ph = mod.(phase,360) .- 180 #Residual
616-
_findCrossings(w, n, ph)
630+
_findCrossings(w, n, ph; filter_th=20.0)
617631
end
618632

619-
function _findCrossings(w, n, res)
633+
function _findCrossings(w, n, res; filter_th=Inf)
620634
wcross = Vector{eltype(w)}()
621635
tcross = Vector{eltype(w)}()
622636
for i in 1:(length(w)-1)
623637
if res[i] == 0
624638
push!(wcross, w[i])
625639
push!(tcross, i)
626640
elseif n[i] != n[i+1]
641+
d = res[i]-res[i+1]
642+
if abs(d) > filter_th && (sign(w[i+1]) != sign(w[i]))
643+
# This can happen when there are both negative and positive frequencies and the Nyquist contour crosses the negative real axis at -infinity
644+
# This logic is slightly flawed since if there are two frequencies, like -eps, eps that have phase values very close to each other we'll fail to reject the crossing, but to handle this case properly one would have to look at both phase and gain at the same time, which we aren't set-up to do easily at this. We should be able to reject the most common case when the default eps() negative frequency is the only one used
645+
continue
646+
end
627647
#Interpolate to approximate crossing
628-
t = res[i]/(res[i]-res[i+1])
648+
t = res[i]/d
629649
push!(tcross, i+t)
630650
wt = w[i]+t*(w[i+1]-w[i])
631651
push!(wcross, wt)
@@ -649,11 +669,14 @@ The delay margin is computed as the phase margin in radians divided by the cross
649669
function delaymargin(G::LTISystem)
650670
# Phase margin in radians divided by cross-over frequency in rad/s.
651671
issiso(G) || error("delaymargin only supports SISO systems")
652-
m = margin(G,allMargins=true)
653-
isempty(m[4][1]) && return Inf
654-
ϕₘ, i = findmin(m[4][1])
672+
ωgₘ, gₘ, ωϕₘa, ϕₘa = margin(G,allMargins=true)
673+
isempty(ϕₘa[1]) && return Inf
674+
ϕₘ, i = findmin(sign.(ωϕₘa[1]) .* ϕₘa[1]) # flip sign of negative frequency margins
655675
ϕₘ *= π/180
656-
ωϕₘ = m[3][1][i]
676+
ωϕₘ = ωϕₘa[1][i]
677+
if numeric_type(G) <: Real
678+
ωϕₘ = abs(ωϕₘ)
679+
end
657680
dₘ = ϕₘ/ωϕₘ
658681
if isdiscrete(G)
659682
dₘ /= G.Ts # Give delay margin in number of sample times, as matlab does

lib/ControlSystemsBase/test/test_analysis.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@test_throws MethodError poles(big(1.0)*ssrand(1,1,1)) # This errors before loading GenericSchur
22
using GenericSchur # Required to compute eigvals (in tzeros and poles) of a matrix with exotic element types
3+
using Test
34

45
es(x) = sort(x, by=LinearAlgebra.eigsortby)
56
## tzeros ##
@@ -209,6 +210,21 @@ G = [1/(s+2) -1/(s+2); 1/(s+2) (s+1)/(s+2)]
209210

210211
## MARGIN ##
211212

213+
# Test case that requires negative frequencies to be included in the grid in order to find one margin
214+
# https://github.com/JuliaControl/ControlSystems.jl/issues/1045
215+
temp = let
216+
tempA = [-1.42119805189796 1.0 1.42119805189796 0.0; -1.5105265612217906 -1.4146551592967844 1.009901951359278 0.0; 0.0 0.0 0.0 1.0; 0.0 0.0 0.5 0.0]
217+
tempB = [0.0; 0.0; 0.0; 0.1;;]
218+
tempC = [10.006246098625127 14.146551592967842 0.0 0.0]
219+
tempD = [0.0;;]
220+
ss(tempA, tempB, tempC, tempD)
221+
end
222+
wgm, gm, wpm, pm = margin(temp, allMargins=true)
223+
@test wgm[] [0.0, 1.2311038829891778] atol=1e-3
224+
@test gm[] [0.8733647564616344, 2.005125180939021] atol=1e-3
225+
226+
227+
212228
w = exp10.(LinRange(-1, 2, 100))
213229
P = tf(1,[1.0, 1])
214230
ωgₘ, gₘ, ωϕₘ, ϕₘ = margin(P, w)
@@ -231,6 +247,7 @@ marginplot(P, w)
231247
@test ϕₘ[][] 50
232248
@test ωϕₘ[][] 0.7871132039227572
233249

250+
234251
## Delay margin
235252
dm = delaymargin(P)[]
236253
R = freqresp(P*delay(dm), ωϕₘ[][]-0.5:0.0001:ωϕₘ[][]+0.5)

0 commit comments

Comments
 (0)