Skip to content

Commit 94c084a

Browse files
committed
add gainphaseplot
1 parent d1dc7fb commit 94c084a

3 files changed

Lines changed: 114 additions & 44 deletions

File tree

docs/src/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,12 @@ dms = diskmargin(L, 0, w)
253253
plot(dms)
254254
```
255255

256+
We can also visualize the plane of complex perturbations that are allowed to be simultaneously applied to the system:
257+
```@example diskmargin
258+
gainphaseplot(L)
259+
```
260+
The green regions are stable perturbations while red regions are unstable. The diskmargin is the largest disk that can be placed entirely inside the green area. The center of the disk is determined by the skew of the diskmargin. The classical gain margin is the length of the green area along the x-axis starting at the point 1, while the classical phase margin is the length of the green area along the unit circle starting a the point 1.
261+
256262

257263
## Closed-loop analysis
258264
- [`output_sensitivity`](@ref)

src/diskmargin.jl

Lines changed: 107 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -257,59 +257,64 @@ end
257257
end
258258
end
259259

260-
@recipe function plotdiskmargin(dm::AbstractVector{<:Diskmargin}; lower=true, phase=true)
260+
@recipe function plotdiskmargin(dm::AbstractVector{<:Diskmargin}; lower=true, phase=true, gain=true)
261+
gain || phase || error("Must plot either gain or phase")
261262
w = [dm.ω0 for dm in dm]
262-
layout --> (phase ? 2 : 1, 1)
263+
layout --> (phase + gain, 1)
263264
link --> :x
264265
gma = getfield.(dm, :γmax)
265266
gmi = getfield.(dm, :γmin)
266267
# ylims --> (0, min(10, maximum(gma)))
267268
data = (lower ? [gmi gma] : gma)
268269
data = max.(0, data)
269270
replace!(data, 0 => -Inf)
270-
@series begin
271-
subplot --> 1
272-
title --> "Gain margin"
273-
label --> (lower ? ["Lower" "Upper"] : "Upper")
274-
# xguide --> "Frequency"
275-
xscale --> :log10
276-
yscale --> :log10
277-
w, data
278-
end
279-
replace!(data, -Inf => Inf)
280-
m,i = findmin(data[:, end])
281-
@series begin
282-
subplot --> 1
283-
primary := true
284-
seriestype := :scatter
285-
seriescolor := :red
286-
label := string(round(m, sigdigits=3))
287-
[w[i]], [m]
288-
end
289-
@series begin
290-
primary := false
291-
seriestype := :hline
292-
linecolor := :black
293-
[1]
294-
end
295-
phase || return
296-
ϕm = getfield.(dm, :ϕm)
297-
m,i = findmin(ϕm)
298-
@series begin
299-
subplot --> 2
300-
primary := true
301-
seriestype := :scatter
302-
seriescolor := :red
303-
label := string(round(m, sigdigits=3))
304-
[w[i]], [m]
271+
@show gain, phase
272+
if gain
273+
@series begin
274+
# subplot --> 1
275+
title --> "Gain margin"
276+
label --> (lower ? ["Lower" "Upper"] : "Upper")
277+
# xguide --> "Frequency"
278+
xscale --> :log10
279+
yscale --> :log10
280+
w, data
281+
end
282+
replace!(data, -Inf => Inf)
283+
m,i = findmin(data[:, end])
284+
@series begin
285+
# subplot --> 1
286+
primary := true
287+
seriestype := :scatter
288+
seriescolor := :red
289+
label := string(round(m, sigdigits=3))
290+
[w[i]], [m]
291+
end
292+
@series begin
293+
primary := false
294+
seriestype := :hline
295+
linecolor := :black
296+
[1]
297+
end
305298
end
306-
@series begin
307-
subplot --> 2
308-
title --> "Phase margin (deg)"
309-
xguide --> "Frequency"
310-
xscale --> :log10
311-
label --> ""
312-
w, ϕm
299+
if phase
300+
ϕm = getfield.(dm, :ϕm)
301+
m,i = findmin(ϕm)
302+
@series begin
303+
subplot --> (phase + gain)
304+
primary := true
305+
seriestype := :scatter
306+
seriescolor := :red
307+
label := string(round(m, sigdigits=3))
308+
[w[i]], [m]
309+
end
310+
@series begin
311+
subplot --> (phase + gain)
312+
title --> "Phase margin (deg)"
313+
xguide --> "Frequency"
314+
xscale --> :log10
315+
label --> ""
316+
w, ϕm
317+
end
313318
end
314319
end
315320

@@ -354,6 +359,64 @@ end
354359
end
355360
end
356361

362+
@userplot GainPhasePlot
363+
364+
@recipe function f(gp::GainPhasePlot)
365+
if length(gp.args) == 3
366+
P, re, im = gp.args
367+
elseif length(gp.args) == 1
368+
P = gp.args[1]
369+
re = LinRange(-1.5, 10, 300)
370+
im = LinRange(-3, 3, 300)
371+
else
372+
error("gainphaseplot takes either 1 (sys) or 3 arguments (sys, re, im).")
373+
end
374+
375+
res = map(Iterators.product(re,im)) do (r,i)
376+
isstable(feedback(P*complex(r,i)))
377+
end
378+
colorbar --> false
379+
aspect_ratio --> 1
380+
@series begin
381+
color --> [:red, :green]
382+
seriesalpha --> 0.5
383+
seriestype := :heatmap
384+
re, im, res'
385+
end
386+
θ = LinRange(0, 2π, 100)
387+
S = sin.(θ)
388+
C = cos.(θ)
389+
framestyle := :zerolines
390+
@series begin
391+
primary := false
392+
linestyle := :dash
393+
seriescolor := :black
394+
C, S
395+
end
396+
r = [0, 1.5]
397+
c = [:red, :orange, :green]
398+
for (i, θ) = enumerate([30, 45, 60])
399+
points = r*cis(deg2rad(θ))
400+
@series begin
401+
linestyle := :dash
402+
linewidth --> 2
403+
xguide --> "Re"
404+
yguide --> "Im"
405+
seriescolor --> c[i]
406+
label --> string(θ)*"°"
407+
real(points), imag(points)
408+
end
409+
end
410+
end
411+
412+
"""
413+
gainphaseplot(P)
414+
gainphaseplot(P, re, im)
415+
416+
Plot complex perturbantions to the plant `P` and indicate whether or not the closed-loop system is stable. The diskmargin is the largest disk that can be fit inside the green region that only contains stable variations.
417+
"""
418+
gainphaseplot
419+
357420

358421
"""
359422
passivity_index(P; kwargs...)

test/test_diskmargin.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ plot(dm)
1414
nyquistplot(L)
1515
plot!(dm, nyquist=true)
1616
plot!(Disk(dm), nyquist=true)
17+
gainphaseplot(L)
1718

1819
@test dm.alpha == dm.α
1920
@test length(dm.gainmargin) == 2

0 commit comments

Comments
 (0)