You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
stability_margins: 'poly' (and default 'best') returns spurious gain crossovers for discrete-time systems of moderate order; the root tolerance eps**(1/n) grows with degree #1266
For discrete-time systems, stability_margins with method='poly', and so with the default 'best', returns gain crossovers where |L| is nowhere near 1, and with them a wrong phase margin. It starts at modest orders. For a moving average, L = T/(1 - T) with T(z) = (1/N)(z^-1 + ... + z^-N):
N
true pm
poly / best
frd
8
69.009
69.009
69.009
17
70.212
5.687
70.212
144
71.250
1.270
71.250
(control 0.10.2, numpy 2.5.4. "True" is from a dense grid with the one gain crossing refined by bisection.)
At N = 144, poly reports wgc ≈ 3.098 rad/sample, where |L| ≈ 0.0001.
Cause._poly_z_mag1_crossing and _poly_z_real_crossing keep a root of num(z)num(1/z) - den(z)den(1/z) if ||z| - 1| < eps**(1/len(p2)) (margins.py lines 119 and 135 on main). That tolerance grows with degree: 0.36 at N = 17, 0.88 at N = 144. At N = 144, 143 roots with |z| from 0.962 to 1.040 are kept, and nothing checks |L| at them. best doesn't fall back, because _likely_numerical_inaccuracy compares coefficient norms, not degree.
Suggested fix. Accept a candidate only if abs(abs(L(exp(1j*w))) - 1) (or the imaginary part, for phase crossings) is small, to a tolerance independent of degree. Making _likely_numerical_inaccuracy consider degree would also send best to frd.
Workaround. Pass an FRD on a grid that covers the crossover. Plain method='frd' can miss a low-frequency one: for 0.02 / (z^5 (z - 1)) it returns pm = inf, where poly is right.
Related but different: #523, fixed for a low-order example.
@gimballock You opened two big issues within 2 minutes of each other, so I assume you are using a chat bot to generate some or all of the above. Please add a disclosure about tool use: https://numpy.org/doc/stable/dev/ai_policy.html
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.
Activity
slivingston commentedon Nov 5, 2026
@gimballock You opened two big issues within 2 minutes of each other, so I assume you are using a chat bot to generate some or all of the above. Please add a disclosure about tool use: https://numpy.org/doc/stable/dev/ai_policy.html
gimballock commentedon Nov 5, 2026
Disclosure: yes, this was written with an AI agent, Claude Code (Anthropic's Claude Opus 5.5 model).
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.I'm responsible for what's posted under my account, and questions are welcome.