-
Notifications
You must be signed in to change notification settings - Fork 438
Disk margin calculations #1146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Disk margin calculations #1146
Conversation
…andler comment, fix typo in skew description of disk_margins docstring
…e function to plot allowable gain/phase variations.
…n the margins module
calculation of 'f', the bounding complex curve. Seems to look correct for balanced (skew = 0) case, still verifying the skewed equivalent.
…on-control into jdelange/disk-margins
…ol/tests/margin_test.py
control/margins.py
Outdated
try: | ||
from . import mag2db | ||
except ImportError: | ||
# Likely due the following circular import issue: | ||
# | ||
# 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try to import from the module where this is defined:
from .ctrlutil import mag2db
control/margins.py
Outdated
>> | ||
>> omega = np.logspace(-1, 3, 1001) | ||
>> | ||
>> P = control.ss([[0, 10],[-10, 0]], np.eye(2), [[1, 10], [-10, 1]], [[0, 0],[0, 0]]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example seems too big for a docstring, and anyway, it already is given in examples/disk_margins.py. Is there a smaller example that can be given in this docstring?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. Yes - will remove all the plotting.
I fixed the docstrings and implemented the linter's recommendations so that automated workflows finally pass. One thing I previously hadn't appreciated was the complexity of the control plotting code, so to make this PR simpler I'm only proposing to add a library function to calculate the disk-based margins. All other plotting is left to user code (or future PRs), e.g. the local function |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some additional suggestions attached, mainly focused on coding style (to be consistent with the standard style in the package).
control/tests/margin_test.py
Outdated
assert_allclose([DMo], [0.3754], atol = 0.1) # disk margin of 0.3754 | ||
assert_allclose([DGMo], [3.3], atol = 0.1) # disk-based gain margin of 3.3 dB | ||
assert_allclose([DPMo], [21.26], atol = 0.1) # disk-based phase margin of 21.26 deg | ||
|
||
# Balanced (S - T) disk-based stability margins at plant input | ||
DMi, DGMi, DPMi = disk_margins(Li, omega, skew = 0.0) | ||
assert_allclose([DMi], [0.3754], atol = 0.1) # disk margin of 0.3754 | ||
assert_allclose([DGMi], [3.3], atol = 0.1) # disk-based gain margin of 3.3 dB | ||
assert_allclose([DPMi], [21.26], atol = 0.1) # disk-based phase margin of 21.26 deg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code never runs if slycot is not loaded => can you just remove it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes - will fix.
control/tests/margin_test.py
Outdated
Lo = P*K # loop transfer function, broken at plant output | ||
Li = K*P # loop transfer function, broken at plant input | ||
|
||
if importlib.util.find_spec('slycot') == None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just use ct.slycot_check()
here and avoid using the importlib
call.
control/margins.py
Outdated
@@ -517,3 +521,130 @@ def margin(*args): | |||
% len(args)) | |||
|
|||
return margin[0], margin[1], margin[3], margin[4] | |||
|
|||
def disk_margins(L, omega, skew = 0.0, returnall = False): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove spaces around =
to be consistent with python-control (PEP8) coding conventions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will fix
control/margins.py
Outdated
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which of these is the default value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will update. Default of 0.0 is consistent with the MATLAB function diskmargin
.
control/margins.py
Outdated
skew = 1 uses the sensitivity function S | ||
skew = -1 uses the complementary sensitivity function T | ||
returnall : bool, optional | ||
If true, return frequency-dependent margins. If False (default), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true → True
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will fix
control/margins.py
Outdated
DM = np.zeros(omega.shape, np.float64) | ||
DGM = np.zeros(omega.shape, np.float64) | ||
DPM = np.zeros(omega.shape, np.float64) | ||
for ii in range(0,len(omega)): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing space after comma (PEP8).
control/margins.py
Outdated
DPM = np.zeros(omega.shape, np.float64) | ||
for ii in range(0,len(omega)): | ||
# Disk margin (a.k.a. "alpha") vs. frequency | ||
if L.issiso() and (ab13md == None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parentheses around second expression are not needed.
control/margins.py
Outdated
np.array(num_loops*[2]))[0] | ||
|
||
# Disk-based gain margin (dB) and phase margin (deg) | ||
with np.errstate(divide = 'ignore', invalid = 'ignore'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove excess spaces around keyword assignment operator.
control/margins.py
Outdated
|
||
if returnall: | ||
# Frequency-dependent disk margin, gain margin and phase margin | ||
return (DM, DGM, DPM) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parentheses not needed here.
control/margins.py
Outdated
return ((not DM.shape[0] and float('inf')) or np.amin(DM), | ||
(not gmidx != -1 and float('inf')) or DGM[gmidx][0], | ||
(not DPM.shape[0] and float('inf')) or DPM[pmidx][0]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found this very hard to parse. I wonder whether using if-else syntax might be simpler:
return (
float('inf') if DM.shape[0] == 0 else np.amin(DM),
float('inf') if gmidx == -1 else DGM[gmidx][0],
float('inf') if DPM.shape[0] == 0 else DPM[pmidx][0])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree - thanks for the snippet, that is much cleaner. I tried to emulate stability_margins
, but had whittled it down somewhat. Will update.
…reference on disk/ellipse-based margin calculations.
@murrayrm (and @slivingston) thanks for taking time to review. I think everything's addressed - but let me know of anything else! |
Am relatively new to this toolbox, have been using it here and there and found #726.
This is an initial prototype Python implementation of disk margins, built on top of$\mu$ at each discrete frequency. The function
python-control
andslycot
. The code should work both for SISO and MIMO systems, the latter of which requiresslycot
(for SLICOT'sAB13MD
) to compute the upper bound ofdisk_margins
computes disk margins (and corresponding disk-based gain/phase margins), optionally returning the whole frequency-dependent vectors for further plotting.It's been verified against the example SISO loop transfer functions in the published paper and the "spinning satellite" MIMO example from the MathWorks documentation. I also confirmed SISO disk margins match the relevant output of existing function
stability_margins
corresponding to the Nyquist plot's distance to -1. All seem to match within a few significant digits, so the general behavior seems correct. Might be good to get another set of eyes to double check/test further.I tried to base as much as possible (e.g. style conventions) on existing code in
control/margins.py
. Example usage (seeexamples/disk_margin.py
):Output:
The example script also shows how to plot the allowable/stable region of gain and phase variations which will not destabilize the loop, in a local function
plot_allowable_region
, e.g.