Skip to content

Commit e8897f6

Browse files
committed
Address @murrayrm review comments.
1 parent cc02712 commit e8897f6

File tree

2 files changed

+84
-110
lines changed

2 files changed

+84
-110
lines changed

control/margins.py

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ def margin(*args):
522522

523523
return margin[0], margin[1], margin[3], margin[4]
524524

525-
def disk_margins(L, omega, skew = 0.0, returnall = False):
525+
def disk_margins(L, omega, skew=0.0, returnall=False):
526526
"""Compute disk-based stability margins for SISO or MIMO LTI
527527
loop transfer function.
528528
@@ -535,11 +535,11 @@ def disk_margins(L, omega, skew = 0.0, returnall = False):
535535
to evaluate the disk-based stability margins
536536
skew : float or array_like, optional
537537
skew parameter(s) for disk margin calculation.
538-
skew = 0 uses the "balanced" sensitivity function 0.5*(S - T)
539-
skew = 1 uses the sensitivity function S
540-
skew = -1 uses the complementary sensitivity function T
538+
skew = 0.0 (default) uses the "balanced" sensitivity function 0.5*(S - T)
539+
skew = 1.0 uses the sensitivity function S
540+
skew = -1.0 uses the complementary sensitivity function T
541541
returnall : bool, optional
542-
If true, return frequency-dependent margins. If False (default),
542+
If True, return frequency-dependent margins. If False (default),
543543
return only the worst-case (minimum) margins.
544544
545545
Returns
@@ -554,11 +554,10 @@ def disk_margins(L, omega, skew = 0.0, returnall = False):
554554
Example
555555
--------
556556
>> omega = np.logspace(-1, 3, 1001)
557-
>> P = control.ss([[0,10],[-10,0]],np.eye(2),[[1,10],\
558-
[-10,1]],[[0,0],[0,0]])
559-
>> K = control.ss([],[],[],[[1,-2],[0,1]])
560-
>> L = P*K
561-
>> DM, DGM, DPM = control.disk_margins(L, omega, skew = 0.0)
557+
>> P = control.ss([[0, 10], [-10, 0]], np.eye(2), [[1, 10], [-10, 1]], 0)
558+
>> K = control.ss([], [], [], [[1, -2], [0, 1]])
559+
>> L = P * K
560+
>> DM, DGM, DPM = control.disk_margins(L, omega, skew=0.0)
562561
"""
563562

564563
# First argument must be a system
@@ -571,7 +570,7 @@ def disk_margins(L, omega, skew = 0.0, returnall = False):
571570
raise ValueError("Loop gain must be square (n_inputs = n_outputs)")
572571

573572
# Need slycot if L is MIMO, for mu calculation
574-
if (not L.issiso()) and (ab13md == None):
573+
if not L.issiso() and ab13md == None:
575574
raise ControlMIMONotImplemented(\
576575
"Need slycot to compute MIMO disk_margins")
577576

@@ -584,56 +583,58 @@ def disk_margins(L, omega, skew = 0.0, returnall = False):
584583

585584
# Compute frequency response of the "balanced" (according
586585
# to the skew parameter "sigma") sensitivity function [1-2]
587-
ST = S + 0.5*(skew - 1)*I
586+
ST = S + 0.5 * (skew - 1) * I
588587
ST_mag, ST_phase, _ = ST.frequency_response(omega)
589-
ST_jw = (ST_mag*np.exp(1j*ST_phase))
588+
ST_jw = (ST_mag * np.exp(1j * ST_phase))
590589
if not L.issiso():
591-
ST_jw = ST_jw.transpose(2,0,1)
590+
ST_jw = ST_jw.transpose(2, 0, 1)
592591

593592
# Frequency-dependent complex disk margin, computed using upper bound of
594593
# the structured singular value, a.k.a. "mu", of (S + (skew - I)/2).
595-
DM = np.zeros(omega.shape, np.float64)
596-
DGM = np.zeros(omega.shape, np.float64)
597-
DPM = np.zeros(omega.shape, np.float64)
598-
for ii in range(0,len(omega)):
594+
DM = np.zeros(omega.shape)
595+
DGM = np.zeros(omega.shape)
596+
DPM = np.zeros(omega.shape)
597+
for ii in range(0, len(omega)):
599598
# Disk margin (a.k.a. "alpha") vs. frequency
600-
if L.issiso() and (ab13md == None):
599+
if L.issiso() and ab13md == None:
601600
# For the SISO case, the norm on (S + (skew - I)/2) is
602601
# unstructured, and can be computed as the magnitude
603602
# of the frequency response.
604-
DM[ii] = 1.0/ST_mag[ii]
603+
DM[ii] = 1.0 / ST_mag[ii]
605604
else:
606605
# For the MIMO case, the norm on (S + (skew - I)/2) assumes a
607606
# single complex uncertainty block diagonal uncertainty
608607
# structure. AB13MD provides an upper bound on this norm at
609608
# the given frequency omega[ii].
610-
DM[ii] = 1.0/ab13md(ST_jw[ii], np.array(num_loops*[1]),\
611-
np.array(num_loops*[2]))[0]
609+
DM[ii] = 1.0 / ab13md(ST_jw[ii], np.array(num_loops * [1]),\
610+
np.array(num_loops * [2]))[0]
612611

613612
# Disk-based gain margin (dB) and phase margin (deg)
614-
with np.errstate(divide = 'ignore', invalid = 'ignore'):
613+
with np.errstate(divide='ignore', invalid='ignore'):
615614
# Real-axis intercepts with the disk
616-
gamma_min = (1 - 0.5*DM[ii]*(1 - skew))/(1 + 0.5*DM[ii]*(1 + skew))
617-
gamma_max = (1 + 0.5*DM[ii]*(1 - skew))/(1 - 0.5*DM[ii]*(1 + skew))
615+
gamma_min = (1 - 0.5 * DM[ii] * (1 - skew)) \
616+
/ (1 + 0.5 * DM[ii] * (1 + skew))
617+
gamma_max = (1 + 0.5 * DM[ii] * (1 - skew)) \
618+
/ (1 - 0.5 * DM[ii] * (1 + skew))
618619

619620
# Gain margin (dB)
620-
DGM[ii] = mag2db(np.minimum(1/gamma_min, gamma_max))
621+
DGM[ii] = mag2db(np.minimum(1 / gamma_min, gamma_max))
621622
if np.isnan(DGM[ii]):
622623
DGM[ii] = float('inf')
623624

624625
# Phase margin (deg)
625626
if np.isinf(gamma_max):
626627
DPM[ii] = 90.0
627628
else:
628-
DPM[ii] = (1 + gamma_min*gamma_max)/(gamma_min + gamma_max)
629+
DPM[ii] = (1 + gamma_min * gamma_max) / (gamma_min + gamma_max)
629630
if abs(DPM[ii]) >= 1.0:
630631
DPM[ii] = float('Inf')
631632
else:
632633
DPM[ii] = np.rad2deg(np.arccos(DPM[ii]))
633634

634635
if returnall:
635636
# Frequency-dependent disk margin, gain margin and phase margin
636-
return (DM, DGM, DPM)
637+
return DM, DGM, DPM
637638
else:
638639
# Worst-case disk margin, gain margin and phase margin
639640
if DGM.shape[0] and not np.isinf(DGM).all():
@@ -645,6 +646,7 @@ def disk_margins(L, omega, skew = 0.0, returnall = False):
645646
if DPM.shape[0]:
646647
pmidx = np.where(DPM == np.min(DPM))
647648

648-
return ((not DM.shape[0] and float('inf')) or np.amin(DM),
649-
(not gmidx != -1 and float('inf')) or DGM[gmidx][0],
650-
(not DPM.shape[0] and float('inf')) or DPM[pmidx][0])
649+
return (
650+
float('inf') if DM.shape[0] == 0 else np.amin(DM),
651+
float('inf') if gmidx == -1 else DGM[gmidx][0],
652+
float('inf') if DPM.shape[0] == 0 else DPM[pmidx][0])

control/tests/margin_test.py

Lines changed: 51 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from control import ControlMIMONotImplemented, FrequencyResponseData, \
1717
StateSpace, TransferFunction, margin, phase_crossover_frequencies, \
1818
stability_margins, disk_margins, tf, ss
19+
from control.exception import slycot_check
1920

2021
s = TransferFunction.s
2122

@@ -382,55 +383,45 @@ def test_siso_disk_margin():
382383
L = tf(25, [1, 10, 10, 10])
383384

384385
# Balanced (S - T) disk-based stability margins
385-
DM, DGM, DPM = disk_margins(L, omega, skew = 0.0)
386-
assert_allclose([DM], [0.46], atol = 0.1) # disk margin of 0.46
387-
assert_allclose([DGM], [4.05], atol = 0.1) # disk-based gain margin of 4.05 dB
388-
assert_allclose([DPM], [25.8], atol = 0.1) # disk-based phase margin of 25.8 deg
386+
DM, DGM, DPM = disk_margins(L, omega, skew=0.0)
387+
assert_allclose([DM], [0.46], atol=0.1) # disk margin of 0.46
388+
assert_allclose([DGM], [4.05], atol=0.1) # disk-based gain margin of 4.05 dB
389+
assert_allclose([DPM], [25.8], atol=0.1) # disk-based phase margin of 25.8 deg
389390

390391
# For SISO systems, the S-based (S) disk margin should match the third output
391392
# of existing library "stability_margins", i.e., minimum distance from the
392393
# Nyquist plot to -1.
393394
_, _, SM = stability_margins(L)[:3]
394-
DM = disk_margins(L, omega, skew = 1.0)[0]
395-
assert_allclose([DM], [SM], atol = 0.01)
395+
DM = disk_margins(L, omega, skew=1.0)[0]
396+
assert_allclose([DM], [SM], atol=0.01)
396397

397398
def test_mimo_disk_margin():
398399
# Frequencies of interest
399400
omega = np.logspace(-1, 3, 1001)
400401

401402
# Loop transfer gain
402-
P = ss([[0, 10],[-10, 0]], np.eye(2), [[1, 10], [-10, 1]], [[0, 0],[0, 0]]) # plant
403-
K = ss([],[],[], [[1, -2], [0, 1]]) # controller
404-
Lo = P*K # loop transfer function, broken at plant output
405-
Li = K*P # loop transfer function, broken at plant input
403+
P = ss([[0, 10], [-10, 0]], np.eye(2), [[1, 10], [-10, 1]], 0) # plant
404+
K = ss([], [], [], [[1, -2], [0, 1]]) # controller
405+
Lo = P * K # loop transfer function, broken at plant output
406+
Li = K * P # loop transfer function, broken at plant input
406407

407-
if importlib.util.find_spec('slycot') == None:
408-
with pytest.raises(ControlMIMONotImplemented,\
409-
match = "Need slycot to compute MIMO disk_margins"):
410-
411-
# Balanced (S - T) disk-based stability margins at plant output
412-
DMo, DGMo, DPMo = disk_margins(Lo, omega, skew = 0.0)
413-
assert_allclose([DMo], [0.3754], atol = 0.1) # disk margin of 0.3754
414-
assert_allclose([DGMo], [3.3], atol = 0.1) # disk-based gain margin of 3.3 dB
415-
assert_allclose([DPMo], [21.26], atol = 0.1) # disk-based phase margin of 21.26 deg
416-
417-
# Balanced (S - T) disk-based stability margins at plant input
418-
DMi, DGMi, DPMi = disk_margins(Li, omega, skew = 0.0)
419-
assert_allclose([DMi], [0.3754], atol = 0.1) # disk margin of 0.3754
420-
assert_allclose([DGMi], [3.3], atol = 0.1) # disk-based gain margin of 3.3 dB
421-
assert_allclose([DPMi], [21.26], atol = 0.1) # disk-based phase margin of 21.26 deg
422-
else:
408+
if slycot_check():
423409
# Balanced (S - T) disk-based stability margins at plant output
424-
DMo, DGMo, DPMo = disk_margins(Lo, omega, skew = 0.0)
425-
assert_allclose([DMo], [0.3754], atol = 0.1) # disk margin of 0.3754
426-
assert_allclose([DGMo], [3.3], atol = 0.1) # disk-based gain margin of 3.3 dB
427-
assert_allclose([DPMo], [21.26], atol = 0.1) # disk-based phase margin of 21.26 deg
410+
DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0)
411+
assert_allclose([DMo], [0.3754], atol=0.1) # disk margin of 0.3754
412+
assert_allclose([DGMo], [3.3], atol=0.1) # disk-based gain margin of 3.3 dB
413+
assert_allclose([DPMo], [21.26], atol=0.1) # disk-based phase margin of 21.26 deg
428414

429415
# Balanced (S - T) disk-based stability margins at plant input
430-
DMi, DGMi, DPMi = disk_margins(Li, omega, skew = 0.0)
431-
assert_allclose([DMi], [0.3754], atol = 0.1) # disk margin of 0.3754
432-
assert_allclose([DGMi], [3.3], atol = 0.1) # disk-based gain margin of 3.3 dB
433-
assert_allclose([DPMi], [21.26], atol = 0.1) # disk-based phase margin of 21.26 deg
416+
DMi, DGMi, DPMi = disk_margins(Li, omega, skew=0.0)
417+
assert_allclose([DMi], [0.3754], atol=0.1) # disk margin of 0.3754
418+
assert_allclose([DGMi], [3.3], atol=0.1) # disk-based gain margin of 3.3 dB
419+
assert_allclose([DPMi], [21.26], atol=0.1) # disk-based phase margin of 21.26 deg
420+
else:
421+
# Slycot not installed. Should throw exception.
422+
with pytest.raises(ControlMIMONotImplemented,\
423+
match="Need slycot to compute MIMO disk_margins"):
424+
DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0)
434425

435426
def test_siso_disk_margin_return_all():
436427
# Frequencies of interest
@@ -440,68 +431,49 @@ def test_siso_disk_margin_return_all():
440431
L = tf(25, [1, 10, 10, 10])
441432

442433
# Balanced (S - T) disk-based stability margins
443-
DM, DGM, DPM = disk_margins(L, omega, skew = 0.0, returnall = True)
434+
DM, DGM, DPM = disk_margins(L, omega, skew=0.0, returnall=True)
444435
assert_allclose([omega[np.argmin(DM)]], [1.94],\
445-
atol = 0.01) # sensitivity peak at 1.94 rad/s
446-
assert_allclose([min(DM)], [0.46], atol = 0.1) # disk margin of 0.46
436+
atol=0.01) # sensitivity peak at 1.94 rad/s
437+
assert_allclose([min(DM)], [0.46], atol=0.1) # disk margin of 0.46
447438
assert_allclose([DGM[np.argmin(DM)]], [4.05],\
448-
atol = 0.1) # disk-based gain margin of 4.05 dB
439+
atol=0.1) # disk-based gain margin of 4.05 dB
449440
assert_allclose([DPM[np.argmin(DM)]], [25.8],\
450-
atol = 0.1) # disk-based phase margin of 25.8 deg
441+
atol=0.1) # disk-based phase margin of 25.8 deg
451442

452443
def test_mimo_disk_margin_return_all():
453444
# Frequencies of interest
454445
omega = np.logspace(-1, 3, 1001)
455446

456447
# Loop transfer gain
457-
P = ss([[0, 10],[-10, 0]], np.eye(2),\
458-
[[1, 10], [-10, 1]], [[0, 0],[0, 0]]) # plant
459-
K = ss([],[],[], [[1, -2], [0, 1]]) # controller
460-
Lo = P*K # loop transfer function, broken at plant output
461-
Li = K*P # loop transfer function, broken at plant input
448+
P = ss([[0, 10], [-10, 0]], np.eye(2),\
449+
[[1, 10], [-10, 1]], 0) # plant
450+
K = ss([], [], [], [[1, -2], [0, 1]]) # controller
451+
Lo = P * K # loop transfer function, broken at plant output
452+
Li = K * P # loop transfer function, broken at plant input
462453

463-
if importlib.util.find_spec('slycot') == None:
464-
with pytest.raises(ControlMIMONotImplemented,\
465-
match = "Need slycot to compute MIMO disk_margins"):
466-
467-
# Balanced (S - T) disk-based stability margins at plant output
468-
DMo, DGMo, DPMo = disk_margins(Lo, omega, skew = 0.0, returnall = True)
469-
assert_allclose([omega[np.argmin(DMo)]], [omega[0]],\
470-
atol = 0.01) # sensitivity peak at 0 rad/s (or smallest provided)
471-
assert_allclose([min(DMo)], [0.3754], atol = 0.1) # disk margin of 0.3754
472-
assert_allclose([DGMo[np.argmin(DMo)]], [3.3],\
473-
atol = 0.1) # disk-based gain margin of 3.3 dB
474-
assert_allclose([DPMo[np.argmin(DMo)]], [21.26],\
475-
atol = 0.1) # disk-based phase margin of 21.26 deg
476-
477-
# Balanced (S - T) disk-based stability margins at plant input
478-
DMi, DGMi, DPMi = disk_margins(Li, omega, skew = 0.0, returnall = True)
479-
assert_allclose([omega[np.argmin(DMi)]], [omega[0]],\
480-
atol = 0.01) # sensitivity peak at 0 rad/s (or smallest provided)
481-
assert_allclose([min(DMi)], [0.3754],\
482-
atol = 0.1) # disk margin of 0.3754
483-
assert_allclose([DGMi[np.argmin(DMi)]], [3.3],\
484-
atol = 0.1) # disk-based gain margin of 3.3 dB
485-
assert_allclose([DPMi[np.argmin(DMi)]], [21.26],\
486-
atol = 0.1) # disk-based phase margin of 21.26 deg
487-
else:
454+
if slycot_check():
488455
# Balanced (S - T) disk-based stability margins at plant output
489-
DMo, DGMo, DPMo = disk_margins(Lo, omega, skew = 0.0, returnall = True)
456+
DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0, returnall=True)
490457
assert_allclose([omega[np.argmin(DMo)]], [omega[0]],\
491-
atol = 0.01) # sensitivity peak at 0 rad/s (or smallest provided)
492-
assert_allclose([min(DMo)], [0.3754], atol = 0.1) # disk margin of 0.3754
458+
atol=0.01) # sensitivity peak at 0 rad/s (or smallest provided)
459+
assert_allclose([min(DMo)], [0.3754], atol=0.1) # disk margin of 0.3754
493460
assert_allclose([DGMo[np.argmin(DMo)]], [3.3],\
494-
atol = 0.1) # disk-based gain margin of 3.3 dB
461+
atol=0.1) # disk-based gain margin of 3.3 dB
495462
assert_allclose([DPMo[np.argmin(DMo)]], [21.26],\
496-
atol = 0.1) # disk-based phase margin of 21.26 deg
463+
atol=0.1) # disk-based phase margin of 21.26 deg
497464

498465
# Balanced (S - T) disk-based stability margins at plant input
499-
DMi, DGMi, DPMi = disk_margins(Li, omega, skew = 0.0, returnall = True)
466+
DMi, DGMi, DPMi = disk_margins(Li, omega, skew=0.0, returnall=True)
500467
assert_allclose([omega[np.argmin(DMi)]], [omega[0]],\
501-
atol = 0.01) # sensitivity peak at 0 rad/s (or smallest provided)
468+
atol=0.01) # sensitivity peak at 0 rad/s (or smallest provided)
502469
assert_allclose([min(DMi)], [0.3754],\
503-
atol = 0.1) # disk margin of 0.3754
470+
atol=0.1) # disk margin of 0.3754
504471
assert_allclose([DGMi[np.argmin(DMi)]], [3.3],\
505-
atol = 0.1) # disk-based gain margin of 3.3 dB
472+
atol=0.1) # disk-based gain margin of 3.3 dB
506473
assert_allclose([DPMi[np.argmin(DMi)]], [21.26],\
507-
atol = 0.1) # disk-based phase margin of 21.26 deg
474+
atol=0.1) # disk-based phase margin of 21.26 deg
475+
else:
476+
# Slycot not installed. Should throw exception.
477+
with pytest.raises(ControlMIMONotImplemented,\
478+
match="Need slycot to compute MIMO disk_margins"):
479+
DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0, returnall=True)

0 commit comments

Comments
 (0)