GeezerVison is showing github.com — stability_margins misses a phase crossover at the Nyquist frequency (w = pi/dt) for discrete-time systems, so gm is inf when L(-1) < 0 · Issue #1267 · python-control/python-control
stability_margins misses a phase crossover at the Nyquist frequency (w = pi/dt) for discrete-time systems, so gm is inf when L(-1) < 0 #1267DescriptionFor a discrete-time loop with L(-1) < 0, stability_margins returns gm = inf under every method. L(-1) = L(e^{jπ}) is real for real coefficients, so when it is negative the Nyquist plot meets the negative real axis at w = π/dt, and the gain margin is -1/L(-1).
The simplest case is an integrator, L(z) = 1/(z - 1): the closed-loop pole z = 1 - K reaches z = -1 at K = 2, so gm = 2.
import warnings
import control as ct
warnings.simplefilter("ignore")
L = ct.tf([1], [1, -1], dt=1)
print(complex(L(-1))) # (-0.5-0j)
for method in ("poly", "best", "frd"):
print(method, ct.stability_margins(L, method=method)[:2]) # gm = inf, pm = 60 for all three (control 0.10.2)
print(ct.feedback(2 * L, 1).poles()) # [-1.]: marginal at K = 2
1.5/(z - 0.5) (true gm 1) and 0.4/(z + 0.2) (true gm 2) also return gm = inf under all three methods.
Cause.
_z_filter keeps only 0 <= arg(z) < π (margins.py line 105 on main), so the phase-crossover root at z = -1 is dropped.
- The
frd path finds phase crossovers as sign changes inside the grid, so one at the grid's end, w = π/dt, is missed.
Suggested fix. Use zarg <= np.pi for the phase crossings, and in the FRD path check w = π/dt for discrete-time systems: if L(e^{jπ}) is negative, count a phase crossover with gm = -1/L(e^{jπ}).
Workaround.
at_pi = complex(L(-1)).real
gm = min(gm, -1 / at_pi) if at_pi < 0 else gm Activityslivingston commented on Nov 5, 2026 gimballock commented on Nov 5, 2026 Disclosure: yes, this was written with an AI agent, Claude Code (Anthropic's Claude Opus 5.5 model).
- How it was used: the agent found the problem while analysing discrete-time control loops for a research project of mine. It wrote the reproduction, ran it against control 0.10.2 and against
main, wrote the issue text, and posted it through the GitHub CLI at my request. That's why the two issues went up within minutes of each other. Later, also at my request, it shortened the body.
- What's AI-generated: all of the text and code in the issue. The numbers are program output from those runs, not model text, and the reproduction runs as posted.
- My part: I asked for the reports to be filed, but didn't review the text before it was posted.
I'm responsible for what's posted under my account, and questions are welcome.
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|