Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f6dada2
Initial version of disk margin calculation and example/test script
josiahdelange Jan 11, 2025
bdb82a5
Merge remote-tracking branch 'origin' into jdelange/disk-margins
josiahdelange Jan 11, 2025
e46f824
Comment updates: update margins.py header, clarify import exception h…
josiahdelange Jan 11, 2025
1e3af88
More work in progress on disk margin calculation, adding new prototyp…
josiahdelange Jan 12, 2025
ba15789
Add disk_margin_plot to subroutine list in comment header in margins.py
josiahdelange Jan 12, 2025
e47ae02
Follow-on to ba157895fee83ecc15bd5c1bcd8f56f4e50778a5, add disk_margi…
josiahdelange Jan 12, 2025
20686fe
Merge branch 'python-control:main' into jdelange/disk-margins
josiahdelange Jan 13, 2025
f84221d
More work in progress on disk_margin_plot. Corrected a typo/bug in the
josiahdelange Jan 14, 2025
edf8040
Merge branch 'jdelange/disk-margins' of github.com:josiahdelange/pyth…
josiahdelange Jan 14, 2025
9d55419
Merge remote-tracking branch 'origin/main' into jdelange/disk-margins
josiahdelange Apr 12, 2025
2cf1545
Further progress/debugging on disk margin calculation + plot utility
josiahdelange Apr 21, 2025
bbf37f0
Merge remote-tracking branch 'origin/main' into jdelange/disk-margins
josiahdelange Apr 21, 2025
c3efe75
Clean up docstring/code for disk_margin_plot
josiahdelange Apr 21, 2025
63c8523
Clean up docstring/code for disk_margin_plot
josiahdelange Apr 21, 2025
cffc3e5
Remove debugging statements, update comments, add unit tests.
josiahdelange Apr 23, 2025
91517f9
Minor change to fix logic to find minimum across DGM, DPM numpy vectors
josiahdelange Apr 24, 2025
86329e0
Rename disk margin example, since unit tests are now written in contr…
josiahdelange Apr 24, 2025
d92fb20
Remove unneeded dependencies from margins.py, used for debugging
josiahdelange Apr 24, 2025
b2a2edc
Minor updates to docstrings
josiahdelange Apr 24, 2025
1f0ee52
Undo d92fb2045a786581741ddb703819f7ae5865a323
josiahdelange Apr 24, 2025
ba41e8c
Minor tweaks to plots in example script for readability
josiahdelange Apr 24, 2025
14eb315
Fix typo in disk_margin_plot.
josiahdelange Apr 24, 2025
0bebc1d
Fix mag2db import hack/workaround and trim down disk_margin docstring.
josiahdelange Apr 25, 2025
87714bd
Add input handling to disk_margin, clean up column width/comments
josiahdelange Apr 26, 2025
c17910f
Move disk_margin_plot out of the library into the example script
josiahdelange Apr 26, 2025
5f34a7b
Recommended changes from the linter
josiahdelange Apr 26, 2025
f0e2d74
Follow-on to 5f34a7bea410715ee1389a36cd8de3e7001ebf34
josiahdelange Apr 26, 2025
a5fcb91
Add disk_margins to function list
josiahdelange Apr 26, 2025
077d538
Whittle down the docstring from disk_margins
josiahdelange Apr 26, 2025
8f0c037
Put more comments in the disk margin example, add example to document…
josiahdelange Apr 26, 2025
ce80819
Fixing docstrings
josiahdelange Apr 26, 2025
397efab
Corrected expected values for 'no-slycot' condition in newly-added un…
josiahdelange Apr 26, 2025
cc02712
Attempt #2 at 397efabbe7ff9dcc11f2c4309ba73d63ed44d742, based on lint…
josiahdelange Apr 26, 2025
e8897f6
Address @murrayrm review comments.
josiahdelange Apr 29, 2025
579f24b
Update formatting per PEP8/@murrayrm review comments. Add additional…
josiahdelange Apr 29, 2025
fe79760
Follow-on to e8897f6fb57d1c9f7b7409055383083cdb59ae68: remove now-unn…
josiahdelange Apr 29, 2025
b85147e
Update formatting per @murrayrm review comments
josiahdelange Jun 22, 2025
7186406
Merge branch 'python-control:main' into jdelange/disk-margins
josiahdelange Jun 22, 2025
eb3af29
Remove temporarily-added string from docstring
josiahdelange Jun 22, 2025
bb06c9e
Minor tweak to docstring to fit the word 'function' back into the des…
josiahdelange Jun 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Initial version of disk margin calculation and example/test script
  • Loading branch information
josiahdelange committed Jan 11, 2025
commit f6dada2b7fb5a683169abb05faddd23d818a1389
157 changes: 154 additions & 3 deletions control/margins.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,23 @@
from . import frdata
from . import freqplot
from .exception import ControlMIMONotImplemented

__all__ = ['stability_margins', 'phase_crossover_frequencies', 'margin']

from . import ss
try:
from slycot import ab13md
except ImportError:
ab13md = None
try:
from . import mag2db
except:
# Likely the following:
#
# ImportError: cannot import name 'mag2db' from partially initialized module
# 'control' (most likely due to a circular import) (control/__init__.py)
#
def mag2db(mag):
return 20*np.log10(mag)

__all__ = ['stability_margins', 'phase_crossover_frequencies', 'margin', 'disk_margins']

# private helper functions
def _poly_iw(sys):
Expand Down Expand Up @@ -501,6 +515,143 @@ def phase_crossover_frequencies(sys):

return omega, gain

def disk_margins(L, omega, skew = 0.0):
"""Compute disk-based stability margins for SISO or MIMO LTI system.

Parameters
----------
L : SISO or MIMO LTI system representing the loop transfer function
omega : ndarray
1d array of (non-negative) frequencies at which to evaluate
the disk-based stability margins
skew : (optional, default = 0) skew parameter for disk margin calculation.
skew = 0 uses the "balanced" sensitivity function 0.5*(S - T)
skew = -1 uses the sensitivity function S
skew = 1 uses the complementary sensitivity function T

Returns
-------
DM : ndarray
1d array of frequency-dependent disk margins. DM is the same
size as "omega" parameter.
GM : ndarray
1d array of frequency-dependent disk-based gain margins, in dB.
GM is the same size as "omega" parameter.
PM : ndarray
1d array of frequency-dependent disk-based phase margins, in deg.
PM is the same size as "omega" parameter.

Examples
--------
>> import control
>> import numpy as np
>> import matplotlib
>> import matplotlib.pyplot as plt
>>
>> omega = np.logspace(-1, 3, 1001)
>> P = control.ss([[0, 10],[-10, 0]], np.eye(2), [[1, 10], [-10, 1]], [[0, 0],[0, 0]])
Comment thread
josiahdelange marked this conversation as resolved.
Outdated
>> K = control.ss([],[],[], [[1, -2], [0, 1]])
>> L = P*K
>> DM, GM, PM = control.disk_margins(L, omega, 0.0) # balanced (S - T)
>> print(f"min(DM) = {min(DM)}")
>> print(f"min(GM) = {min(GM)} dB")
>> print(f"min(PM) = {min(PM)} deg")
>>
>> plt.figure(1)
>> plt.subplot(3,1,1)
>> plt.semilogx(omega, DM, label='$\\alpha$')
>> plt.legend()
>> plt.title('Disk Margin (Outputs)')
>> plt.grid()
>> plt.tight_layout()
>> plt.xlim([omega[0], omega[-1]])
>>
>> plt.figure(1)
>> plt.subplot(3,1,2)
>> plt.semilogx(omega, GM, label='$\\gamma_{m}$')
>> plt.ylabel('Margin (dB)')
>> plt.legend()
>> plt.title('Disk-Based Gain Margin (Outputs)')
>> plt.grid()
>> plt.ylim([0, 40])
>> plt.tight_layout()
>> plt.xlim([omega[0], omega[-1]])
>>
>> plt.figure(1)
>> plt.subplot(3,1,3)
>> plt.semilogx(omega, PM, label='$\\phi_{m}$')
>> plt.ylabel('Margin (deg)')
>> plt.legend()
>> plt.title('Disk-Based Phase Margin (Outputs)')
>> plt.grid()
>> plt.ylim([0, 90])
>> plt.tight_layout()
>> plt.xlim([omega[0], omega[-1]])

References
----------
[1] Blight, James D., R. Lane Dailey, and Dagfinn Gangsaas. “Practical
Control Law Design for Aircraft Using Multivariable Techniques.”
International Journal of Control 59, no. 1 (January 1994): 93-137.
https://doi.org/10.1080/00207179408923071.

[2] Seiler, Peter, Andrew Packard, and Pascal Gahinet. “An Introduction
to Disk Margins [Lecture Notes].” IEEE Control Systems Magazine 40,
no. 5 (October 2020): 78-95.

[3] P. Benner, V. Mehrmann, V. Sima, S. Van Huffel, and A. Varga, "SLICOT
- A Subroutine Library in Systems and Control Theory", Applied and
Computational Control, Signals, and Circuits (Birkhauser), Vol. 1, Ch.
10, pp. 505-546, 1999.

[4] S. Van Huffel, V. Sima, A. Varga, S. Hammarling, and F. Delebecque,
"Development of High Performance Numerical Software for Control", IEEE
Control Systems Magazine, Vol. 24, Nr. 1, Feb., pp. 60-76, 2004.
"""

# Get dimensions of feedback system
ny,_ = ss(L).C.shape
I = ss([], [], [], np.eye(ny))

# Loop sensitivity function
S = I.feedback(L)

# Compute frequency response of the "balanced" (according
# to the skew parameter "sigma") sensitivity function [1-2]
ST = S + (skew - 1)*I/2
ST_mag, ST_phase, _ = ST.frequency_response(omega)
ST_jw = (ST_mag*np.exp(1j*ST_phase))
if not L.issiso():
ST_jw = ST_jw.transpose(2,0,1)

# Frequency-dependent complex disk margin, computed using upper bound of
# the structured singular value, a.k.a. "mu", of (S + (skew - 1)/2).
# Uses SLICOT routine AB13MD to compute. [1,3-4].
DM = np.zeros(omega.shape, np.float64)
GM = np.zeros(omega.shape, np.float64)
PM = np.zeros(omega.shape, np.float64)
for ii in range(0,len(omega)):
# Disk margin (magnitude) vs. frequency
DM[ii] = 1/ab13md(ST_jw[ii], np.array(ny*[1]), np.array(ny*[2]))[0]

# Gain-only margin (dB) vs. frequency
gamma_min = (1 - DM[ii]*(1 - skew)/2)/(1 + DM[ii]*(1 + skew)/2)
gamma_max = (1 + DM[ii]*(1 - skew)/2)/(1 - DM[ii]*(1 + skew)/2)
GM[ii] = mag2db(np.minimum(1/gamma_min, gamma_max))

# Phase-only margin (deg) vs. frequency
if math.isinf(gamma_max):
PM[ii] = 90.0
else:
PM[ii] = (1 + gamma_min*gamma_max)/(gamma_min + gamma_max)
if PM[ii] >= 1.0:
PM[ii] = 0.0 # np.arccos(1.0)
elif PM[ii] <= -1.0:
PM[ii] = float('Inf') # np.arccos(-1.0)
else:
PM[ii] = np.rad2deg(np.arccos(PM[ii]))

return (DM, GM, PM)

Comment thread
josiahdelange marked this conversation as resolved.
def margin(*args):
"""margin(sysdata)
Expand Down
Loading