GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Disk margin calculations by josiahdelange · Pull Request #1146 · python-control/python-control

Conversation

@josiahdelange

@josiahdelange josiahdelange commented Apr 24, 2025 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

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 python-control and slycot. The code should work both for SISO and MIMO systems, the latter of which requires slycot (for SLICOT's AB13MD) to compute the upper bound of $\mu$ at each discrete frequency. The function disk_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 (see examples/disk_margin.py):

import os, sys, math
import numpy as np
import control

import math
import matplotlib
import matplotlib.pyplot as plt
from warnings import warn

import numpy as np
import scipy as sp

# Frequencies of interest
omega = np.logspace(-1, 3, 1001)

# Laplace variable
s = control.tf('s')

# MIMO loop transfer gain for the spinning satellite example
# https://www.mathworks.com/help/robust/ug/mimo-stability-margins-for-spinning-satellite.html
P = control.ss([[0, 10],[-10, 0]], np.eye(2), [[1, 10], [-10, 1]], [[0, 0],[0, 0]]) # plant
C = control.ss([],[],[], [[1, -2], [0, 1]]) # controller
L = P*C # output loop gain

print(f"------------- Sensitivity function (S) -------------")
DM, GM, PM = control.disk_margins(L, omega, skew = 1.0, returnall = True) # S-based (S)
print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})")
print(f"GM = {GM[np.argmin(DM)]} dB")
print(f"PM = {PM[np.argmin(DM)]} deg")
print(f"min(GM) = {min(GM)} dB")
print(f"min(PM) = {min(PM)} deg\n")

plt.figure(3)
plt.subplot(3,3,1)
plt.semilogx(omega, DM, label='$\\alpha$')
plt.legend()
plt.title('Disk Margin')
plt.grid()
plt.xlim([omega[0], omega[-1]])
plt.ylim([0, 2])

plt.figure(3)
plt.subplot(3,3,4)
plt.semilogx(omega, GM, label='$\\gamma_{m}$')
plt.ylabel('Gain Margin (dB)')
plt.legend()
plt.title('Gain-Only Margin')
plt.grid()
plt.xlim([omega[0], omega[-1]])
plt.ylim([0, 40])

plt.figure(3)
plt.subplot(3,3,7)
plt.semilogx(omega, PM, label='$\\phi_{m}$')
plt.ylabel('Phase Margin (deg)')
plt.legend()
plt.title('Phase-Only Margin')
plt.grid()
plt.xlim([omega[0], omega[-1]])
plt.ylim([0, 90])

print(f"------------- Complementary sensitivity function (T) -------------")
DM, GM, PM = control.disk_margins(L, omega, skew = -1.0, returnall = True) # T-based (T)
print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})")
print(f"GM = {GM[np.argmin(DM)]} dB")
print(f"PM = {PM[np.argmin(DM)]} deg")
print(f"min(GM) = {min(GM)} dB")
print(f"min(PM) = {min(PM)} deg\n")

plt.figure(3)
plt.subplot(3,3,2)
plt.semilogx(omega, DM, label='$\\alpha$')
plt.legend()
plt.title('Disk Margin')
plt.grid()
plt.xlim([omega[0], omega[-1]])
plt.ylim([0, 2])

plt.figure(3)
plt.subplot(3,3,5)
plt.semilogx(omega, GM, label='$\\gamma_{m}$')
plt.ylabel('Gain Margin (dB)')
plt.legend()
plt.title('Gain-Only Margin')
plt.grid()
plt.xlim([omega[0], omega[-1]])
plt.ylim([0, 40])

plt.figure(3)
plt.subplot(3,3,8)
plt.semilogx(omega, PM, label='$\\phi_{m}$')
plt.ylabel('Phase Margin (deg)')
plt.legend()
plt.title('Phase-Only Margin')
plt.grid()
plt.xlim([omega[0], omega[-1]])
plt.ylim([0, 90])

print(f"------------- Balanced sensitivity function (S - T) -------------")
DM, GM, PM = control.disk_margins(L, omega, skew = 0.0, returnall = True) # balanced (S - T)
print(f"min(DM) = {min(DM)} (omega = {omega[np.argmin(DM)]})")
print(f"GM = {GM[np.argmin(DM)]} dB")
print(f"PM = {PM[np.argmin(DM)]} deg")
print(f"min(GM) = {min(GM)} dB")
print(f"min(PM) = {min(PM)} deg\n")

plt.figure(3)
plt.subplot(3,3,3)
plt.semilogx(omega, DM, label='$\\alpha$')
plt.legend()
plt.title('Disk Margin')
plt.grid()
plt.xlim([omega[0], omega[-1]])
plt.ylim([0, 2])

plt.figure(3)
plt.subplot(3,3,6)
plt.semilogx(omega, GM, label='$\\gamma_{m}$')
plt.ylabel('Gain Margin (dB)')
plt.legend()
plt.title('Gain-Only Margin')
plt.grid()
plt.xlim([omega[0], omega[-1]])
plt.ylim([0, 40])

plt.figure(3)
plt.subplot(3,3,9)
plt.semilogx(omega, PM, label='$\\phi_{m}$')
plt.ylabel('Phase Margin (deg)')
plt.legend()
plt.title('Phase-Only Margin')
plt.grid()
plt.xlim([omega[0], omega[-1]])
plt.ylim([0, 90])

Output:

------------- Sensitivity function (S) -------------
min(DM) = 0.3697398698410271 (omega = 0.1)
GM = 2.732761944304522 dB
PM = 21.30709883385843 deg
min(GM) = 2.732761944304522 dB
min(PM) = 21.30709883385843 deg

------------- Complementary sensitivity function (T) -------------
min(DM) = 0.3683035923739884 (omega = 0.1)
GM = 2.7236493433653735 dB
PM = 21.223368586091564 deg
min(GM) = 2.7236493433653735 dB
min(PM) = 21.223368586091564 deg

------------- Balanced sensitivity function (S - T) -------------
min(DM) = 0.3769872636944355 (omega = 0.1)
GM = 3.3140985364257256 dB
PM = 21.349285542574695 deg
min(GM) = 3.3140985364257256 dB
min(PM) = 21.349285542574695 deg

Figure_3

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.

.
.
.
DM_plot = []
DM_plot.append(control.disk_margins(L, omega, skew = -1.0)[0]) # T-based (T)
DM_plot.append(control.disk_margins(L, omega, skew = 0.0)[0]) # balanced (S - T)
DM_plot.append(control.disk_margins(L, omega, skew = 1.0)[0]) # S-based (S)
plt.figure(30)
plot_allowable_region(DM_plot, skew = [-1.0, 0.0, 1.0])

Figure_30

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
josiahdelange added 22 commits January 11, 2025 01:12
@josiahdelange
Initial version of disk margin calculation and example/test script
f6dada2
@josiahdelange
Merge remote-tracking branch 'origin' into jdelange/disk-margins
bdb82a5
@josiahdelange
Comment updates: update margins.py header, clarify import exception h…
e46f824
…andler comment, fix typo in skew description of disk_margins docstring
@josiahdelange
More work in progress on disk margin calculation, adding new prototyp…
1e3af88
…e function to plot allowable gain/phase variations.
@josiahdelange
Add disk_margin_plot to subroutine list in comment header in margins.py
ba15789
@josiahdelange
Follow-on to ba15789, add disk_margin_plot to list of functions withi…
e47ae02
…n the margins module
@josiahdelange
Merge branch 'python-control:main' into jdelange/disk-margins
20686fe
@josiahdelange
More work in progress on disk_margin_plot. Corrected a typo/bug in the
f84221d
calculation of 'f', the bounding complex curve.  Seems to look correct for balanced
(skew = 0) case, still verifying the skewed equivalent.
@josiahdelange
Merge branch 'jdelange/disk-margins' of github.com:josiahdelange/pyth…
edf8040
…on-control into jdelange/disk-margins
@josiahdelange
Merge remote-tracking branch 'origin/main' into jdelange/disk-margins
9d55419
@josiahdelange
Further progress/debugging on disk margin calculation + plot utility
2cf1545
@josiahdelange
Merge remote-tracking branch 'origin/main' into jdelange/disk-margins
bbf37f0
@josiahdelange
Clean up docstring/code for disk_margin_plot
c3efe75
@josiahdelange
Clean up docstring/code for disk_margin_plot
63c8523
@josiahdelange
Remove debugging statements, update comments, add unit tests.
cffc3e5
@josiahdelange
Minor change to fix logic to find minimum across DGM, DPM numpy vectors
91517f9
@josiahdelange
Rename disk margin example, since unit tests are now written in contr…
86329e0
…ol/tests/margin_test.py
@josiahdelange
Remove unneeded dependencies from margins.py, used for debugging
d92fb20
@josiahdelange
Minor updates to docstrings
b2a2edc
@josiahdelange
Undo d92fb20
1f0ee52
@josiahdelange
Minor tweaks to plots in example script for readability
ba41e8c
@josiahdelange
Fix typo in disk_margin_plot.
14eb315
slivingston
slivingston reviewed Apr 24, 2025
View reviewed changes
Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

slivingston
slivingston reviewed Apr 24, 2025
View reviewed changes
Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

josiahdelange added 3 commits April 25, 2025 06:57
@josiahdelange
Fix mag2db import hack/workaround and trim down disk_margin docstring.
0bebc1d
@josiahdelange
Add input handling to disk_margin, clean up column width/comments
87714bd
@josiahdelange
Move disk_margin_plot out of the library into the example script
c17910f
@josiahdelange
josiahdelange marked this pull request as draft April 26, 2025 14:29
josiahdelange added 2 commits April 26, 2025 10:32
@josiahdelange
Recommended changes from the linter
5f34a7b
@josiahdelange
Follow-on to 5f34a7b
f0e2d74
josiahdelange added 4 commits April 26, 2025 11:09
@josiahdelange
Add disk_margins to function list
a5fcb91
@josiahdelange
Whittle down the docstring from disk_margins
077d538
@josiahdelange
Put more comments in the disk margin example, add example to document…
8f0c037
…ation
@josiahdelange
Fixing docstrings
ce80819
@josiahdelange

josiahdelange commented Apr 26, 2025

Copy link
Copy Markdown
Contributor Author

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 plot_allowable_region in examples/disk_margins.py.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@josiahdelange
josiahdelange marked this pull request as ready for review April 26, 2025 16:03
josiahdelange added 2 commits April 26, 2025 14:49
@josiahdelange
Corrected expected values for 'no-slycot' condition in newly-added un…
397efab
…it tests
@josiahdelange
Attempt python-control#2 at 397efab, based on linter recommendation
cc02712
@coveralls

coveralls commented Apr 28, 2025 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Coverage Status

coverage: 94.745% (-0.01%) from 94.759%
when pulling bb06c9e on josiahdelange:jdelange/disk-margins
into 34c6d59 on python-control:main.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

murrayrm
murrayrm reviewed Apr 28, 2025
View reviewed changes

@murrayrm murrayrm left a comment

Copy link
Copy Markdown
Member

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).

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread control/tests/margin_test.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/tests/margin_test.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

josiahdelange added 3 commits April 28, 2025 20:04
@josiahdelange
Address @murrayrm review comments.
e8897f6
@josiahdelange
Update formatting per PEP8/@murrayrm review comments. Add additional …
579f24b
…reference on disk/ellipse-based margin calculations.
@josiahdelange
Follow-on to e8897f6: remove now-unnecessary import of importlib
fe79760
@josiahdelange

josiahdelange commented Apr 29, 2025

Copy link
Copy Markdown
Contributor Author

@murrayrm (and @slivingston) thanks for taking time to review. I think everything's addressed - but let me know of anything else!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

murrayrm
murrayrm reviewed Jun 13, 2025
View reviewed changes

@murrayrm murrayrm left a comment

Copy link
Copy Markdown
Member

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.

Found a few more style issues.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread doc/functions.rst Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread examples/disk_margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread control/margins.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

josiahdelange added 4 commits June 21, 2025 20:00
@josiahdelange
Update formatting per @murrayrm review comments
b85147e
@josiahdelange
Merge branch 'python-control:main' into jdelange/disk-margins
7186406
@josiahdelange
Remove temporarily-added string from docstring
eb3af29
@josiahdelange
Minor tweak to docstring to fit the word 'function' back into the des…
bb06c9e
…cription of skew = 0.0
@josiahdelange

josiahdelange commented Jun 25, 2025

Copy link
Copy Markdown
Contributor Author

@murrayrm thanks and latest formatting issues are now fixed. The one failing check "Slycot from source / build-linux (pull_request)" does not seem to be the disk margin tests.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@murrayrm

murrayrm commented Jun 25, 2025

Copy link
Copy Markdown
Member

Agreed. Tracking the failed unit test in #1161. I'll go ahead and merge this since the fail check is unrelated.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@murrayrm
murrayrm merged commit aa92b65 into python-control:main Jun 25, 2025
@murrayrm murrayrm added this to the 0.10.2 milestone Jul 4, 2025
@josiahdelange
josiahdelange deleted the jdelange/disk-margins branch April 13, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@murrayrm murrayrm murrayrm left review comments
@slivingston slivingston slivingston left review comments

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

0.10.2

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

4 participants

@josiahdelange @coveralls @murrayrm @slivingston
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.