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 — Fix stability_margins missing a phase crossover at the Nyquist frequency for discrete-time systems by gimballock · Pull Request #1271 · python-control/python-control

Conversation

@gimballock

@gimballock gimballock commented Nov 5, 2026

Copy link
Copy Markdown

Fixes #1267. This resubmits #1268 by @RoboX2020, which was closed before review; its two commits are unchanged.

What it does, for discrete-time systems:

  • stability_margins checks z = −1 directly: if L(−1) is real and negative, there's a phase crossover at ω = π/dt with gain margin −1/L(−1).
    • In the poly path this is a check after the real-axis search, not a change to _z_filter. That filter is shared with the gain-crossover search, and widening it let 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's off-circle roots through (see the discussion on Fix missing discrete-time Nyquist endpoint in stability margins #1268).
    • In the frd path, and best's fallback to it, L(−1) is taken before the conversion to FRD. User-supplied FRD data is unchanged.
  • phase_crossover_frequencies also returns the real-axis intersection at π/dt.
  • Tests: 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 #1267's three loops under all three methods, a positive endpoint that must not become a crossover, and a moving-average regression for the interaction.

How it was checked:

  • The full test suite: 3,866 passed and 558 skipped, against 3,853 and 558 on main (bfe9380).
  • Gain margins: 12 discrete-time loops up to order ~1,350 (moving averages, EWMAs and difficulty-retarget rules), plus 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 #1267's three, against a dense frequency scan. All 15 match under poly, best and frd; on main, 12 of the 15 return inf.
  • Phase margins: on the 12 loops, identical to main. The moving averages' are still wrong on both, which is 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, not addressed here.

Disclosure: written with an AI agent, Claude Code (Anthropic's Claude Opus 5.5 model). The code is #1268's, by @RoboX2020, unchanged; its author has said it was bot-written. The agent ran the checks above, reviewed the diff and wrote this description. Before posting, it walked me through the change and this description, with a short quiz on the key points: the bug, why the fix checks z = −1 directly, and what it leaves to #1266. I didn't write any of the code.

Sorry, something went wrong.

Uh oh!

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

All reactions
RoboX2020 added 2 commits November 4, 2026 13:21
@RoboX2020
Fix stability_margins missing a phase crossover at the Nyquist frequency
4f168cd
For a discrete-time loop whose phase reaches -180 degrees exactly at
w = pi/dt, stability_margins returned gm = inf under every method.
For real coefficients L(-1) is always real, so this is a common case:
L(z) = 1/(z-1) has a phase crossover at z = -1 with gm = 2, but
'poly', 'frd' and 'best' all reported gm = inf.

- _z_filter kept only unit-circle roots with 0 <= arg(z) < pi, dropping
  the root at z = -1. Now also keep roots whose argument is exactly +pi
  or exactly -pi (the root-finder returns an exact imaginary zero for a
  real root, with either sign) and report the endpoint frequency via
  abs(arg). No tolerance is applied to the angle, so a genuine root
  near, but not at, -1 keeps its computed frequency and its lower-half
  conjugate stays excluded, exactly as before.
- The 'frd' path detects phase crossovers as sign changes inside the
  frequency grid, which excludes w = pi/dt for discrete-time systems.
  When a transfer function is converted to FRD, evaluate L(-1) and count
  it as a phase crossover if it is real and negative and pi/dt >= epsw.
  (Other FRD-detected crossings ignore epsw; that pre-existing behavior
  is unchanged.)

This also makes phase_crossover_frequencies include the real-axis
intersection at w = pi/dt for discrete-time systems, matching the w = 0
endpoint that was already returned; one existing test reference is
updated accordingly.

Adds regression tests: gm at the Nyquist endpoint under all three
methods, a loop with L(-1) > 0 that must keep gm = inf, and a _z_filter
unit test covering the exact endpoint and a nearby conjugate root pair.
The new tests fail at baseline and pass with the fix.

Fixes python-control#1267
@RoboX2020
Restrict Nyquist endpoint handling to phase crossovers
05006bf
@slivingston

slivingston commented Nov 6, 2026

Copy link
Copy Markdown
Member

In explaining your contribution, do not use AI to automatically generate comments, pull request descriptions, or issue descriptions.

https://numpy.org/doc/stable/dev/ai_policy.html

All reactions

Sorry, something went wrong.

Uh oh!

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

@slivingston slivingston closed this Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Uh oh!

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

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

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

Uh oh!

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

3 participants

@gimballock @slivingston @RoboX2020
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.