Skip to content

Commit cffc3e5

Browse files
committed
Remove debugging statements, update comments, add unit tests.
1 parent 63c8523 commit cffc3e5

2 files changed

Lines changed: 117 additions & 10 deletions

File tree

control/margins.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -653,20 +653,23 @@ def disk_margins(L, omega, skew = 0.0, returnall = False):
653653
ST_jw = ST_jw.transpose(2,0,1)
654654

655655
# Frequency-dependent complex disk margin, computed using upper bound of
656-
# the structured singular value, a.k.a. "mu", of (S + (skew - 1)/2).
657-
# Uses SLICOT routine AB13MD to compute. [1,3-4].
658-
DM = np.zeros(omega.shape, np.float64)
659-
DGM = np.zeros(omega.shape, np.float64)
660-
DPM = np.zeros(omega.shape, np.float64)
656+
# the structured singular value, a.k.a. "mu", of (S + (skew - I)/2).
657+
DM = np.zeros(omega.shape, np.float64) # disk margin vs frequency
658+
DGM = np.zeros(omega.shape, np.float64) # disk-based gain margin vs. frequency
659+
DPM = np.zeros(omega.shape, np.float64) # disk-based phase margin vs. frequency
661660
for ii in range(0,len(omega)):
662661
# Disk margin (a.k.a. "alpha") vs. frequency
663662
if L.issiso() and (ab13md == None):
664-
DM[ii] = np.minimum(1e5,
665-
1.0/bode(ST_jw, omega = omega[ii], plot = False)[0])
663+
# For the SISO case, the norm on (S + (skew - I)/2) is
664+
# unstructured, and can be computed as Bode magnitude
665+
DM[ii] = 1.0/bode(ST_jw, omega = omega[ii], plot = False)[0]
666666
else:
667-
DM[ii] = np.minimum(1e5,
668-
1.0/ab13md(ST_jw[ii], np.array(ny*[1]), np.array(ny*[2]))[0])
667+
# For the MIMO case, the norm on (S + (skew - I)/2) assumes a
668+
# single complex uncertainty block diagonal uncertainty structure.
669+
# AB13MD provides an upper bound on this norm at the given frequency.
670+
DM[ii] = 1.0/ab13md(ST_jw[ii], np.array(ny*[1]), np.array(ny*[2]))[0]
669671

672+
# Disk-based gain margin (dB) and phase margin (deg)
670673
with np.errstate(divide = 'ignore', invalid = 'ignore'):
671674
# Real-axis intercepts with the disk
672675
gamma_min = (1 - 0.5*DM[ii]*(1 - skew))/(1 + 0.5*DM[ii]*(1 + skew))

control/tests/margin_test.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from control import ControlMIMONotImplemented, FrequencyResponseData, \
1616
StateSpace, TransferFunction, margin, phase_crossover_frequencies, \
17-
stability_margins
17+
stability_margins, disk_margins, tf, ss
1818

1919
s = TransferFunction.s
2020

@@ -372,3 +372,107 @@ def test_stability_margins_discrete(cnum, cden, dt,
372372
else:
373373
out = stability_margins(tf)
374374
assert_allclose(out, ref, rtol=rtol)
375+
376+
def test_siso_disk_margin():
377+
# Frequencies of interest
378+
omega = np.logspace(-1, 2, 1001)
379+
380+
# Laplace variable
381+
s = tf('s')
382+
383+
# Loop transfer function
384+
L = tf(25, [1, 10, 10, 10])
385+
386+
# Balanced (S - T) disk-based stability margins
387+
DM, DGM, DPM = disk_margins(L, omega, skew = 0.0)
388+
assert_allclose([DM], [0.46], atol = 0.1) # disk margin of 0.46
389+
assert_allclose([DGM], [4.05], atol = 0.1) # disk-based gain margin of 4.05 dB
390+
assert_allclose([DPM], [25.8], atol = 0.1) # disk-based phase margin of 25.8 deg
391+
392+
# For SISO systems, the S-based (S) disk margin should match the third output
393+
# of existing library "stability_margins", i.e., minimum distance from the
394+
# Nyquist plot to -1.
395+
_, _, SM = stability_margins(L)[:3]
396+
DM = disk_margins(L, omega, skew = 1.0)[0]
397+
assert_allclose([DM], [SM], atol = 0.01)
398+
399+
def test_mimo_disk_margin():
400+
# Frequencies of interest
401+
omega = np.logspace(-1, 3, 1001)
402+
403+
# Laplace variable
404+
s = tf('s')
405+
406+
# Loop transfer gain
407+
P = ss([[0, 10],[-10, 0]], np.eye(2), [[1, 10], [-10, 1]], [[0, 0],[0, 0]]) # plant
408+
K = ss([],[],[], [[1, -2], [0, 1]]) # controller
409+
Lo = P*K # loop transfer function, broken at plant output
410+
Li = K*P # loop transfer function, broken at plant input
411+
412+
# Balanced (S - T) disk-based stability margins at plant output
413+
DMo, DGMo, DPMo = disk_margins(Lo, omega, skew = 0.0)
414+
assert_allclose([DMo], [0.3754], atol = 0.1) # disk margin of 0.3754
415+
assert_allclose([DGMo], [3.3], atol = 0.1) # disk-based gain margin of 3.3 dB
416+
assert_allclose([DPMo], [21.26], atol = 0.1) # disk-based phase margin of 21.26 deg
417+
418+
# Balanced (S - T) disk-based stability margins at plant input
419+
DMi, DGMi, DPMi = disk_margins(Li, omega, skew = 0.0)
420+
assert_allclose([DMi], [0.3754], atol = 0.1) # disk margin of 0.3754
421+
assert_allclose([DGMi], [3.3], atol = 0.1) # disk-based gain margin of 3.3 dB
422+
assert_allclose([DPMi], [21.26], atol = 0.1) # disk-based phase margin of 21.26 deg
423+
424+
def test_siso_disk_margin_return_all():
425+
# Frequencies of interest
426+
omega = np.logspace(-1, 2, 1001)
427+
428+
# Laplace variable
429+
s = tf('s')
430+
431+
# Loop transfer function
432+
L = tf(25, [1, 10, 10, 10])
433+
434+
# Balanced (S - T) disk-based stability margins
435+
DM, DGM, DPM = disk_margins(L, omega, skew = 0.0, returnall = True)
436+
assert_allclose([omega[np.argmin(DM)]], [1.94],\
437+
atol = 0.01) # sensitivity peak at 1.94 rad/s
438+
assert_allclose([min(DM)], [0.46], atol = 0.1) # disk margin of 0.46
439+
assert_allclose([DGM[np.argmin(DM)]], [4.05],\
440+
atol = 0.1) # disk-based gain margin of 4.05 dB
441+
assert_allclose([DPM[np.argmin(DM)]], [25.8],\
442+
atol = 0.1) # disk-based phase margin of 25.8 deg
443+
444+
def test_mimo_disk_margin_return_all():
445+
# Frequencies of interest
446+
omega = np.logspace(-1, 3, 1001)
447+
448+
# Laplace variable
449+
s = tf('s')
450+
451+
# Loop transfer gain
452+
P = ss([[0, 10],[-10, 0]], np.eye(2),\
453+
[[1, 10], [-10, 1]], [[0, 0],[0, 0]]) # plant
454+
K = ss([],[],[], [[1, -2], [0, 1]]) # controller
455+
Lo = P*K # loop transfer function, broken at plant output
456+
Li = K*P # loop transfer function, broken at plant input
457+
458+
# Balanced (S - T) disk-based stability margins at plant output
459+
DMo, DGMo, DPMo = disk_margins(Lo, omega, skew = 0.0, returnall = True)
460+
assert_allclose([omega[np.argmin(DMo)]], [omega[0]],\
461+
atol = 0.01) # sensitivity peak at 0 rad/s (or smallest provided)
462+
assert_allclose([min(DMo)], [0.3754], atol = 0.1) # disk margin of 0.3754
463+
assert_allclose([DGMo[np.argmin(DMo)]], [3.3],\
464+
atol = 0.1) # disk-based gain margin of 3.3 dB
465+
assert_allclose([DPMo[np.argmin(DMo)]], [21.26],\
466+
atol = 0.1) # disk-based phase margin of 21.26 deg
467+
468+
# Balanced (S - T) disk-based stability margins at plant input
469+
DMi, DGMi, DPMi = disk_margins(Li, omega, skew = 0.0, returnall = True)
470+
assert_allclose([omega[np.argmin(DMi)]], [omega[0]],\
471+
atol = 0.01) # sensitivity peak at 0 rad/s (or smallest provided)
472+
assert_allclose([min(DMi)], [0.3754],\
473+
atol = 0.1) # disk margin of 0.3754
474+
assert_allclose([DGMi[np.argmin(DMi)]], [3.3],\
475+
atol = 0.1) # disk-based gain margin of 3.3 dB
476+
assert_allclose([DPMi[np.argmin(DMi)]], [21.26],\
477+
atol = 0.1) # disk-based phase margin of 21.26 deg
478+

0 commit comments

Comments
 (0)