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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy