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 — 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 · Issue #1266 · python-control/python-control

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

New issue
Copy link
New issue
Copy link
Open
Open
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
Copy link

Description

@gimballock
gimballock
opened on Nov 4, 2026
Issue body actions

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

import warnings
import numpy as np
import control as ct

warnings.simplefilter("ignore")
for N in (8, 17, 144):
    num = np.r_[0.0, np.full(N, 1.0 / N)]          # T over z^N
    L = ct.tf(num, np.polysub(np.r_[1.0, np.zeros(N)], num), dt=1)
    for method in ("poly", "best", "frd"):
        gm, pm, sm, wpc, wgc, wms = ct.stability_margins(L, method=method)
        print(N, method, round(pm, 3), "|L| at wgc:", round(abs(complex(L(np.exp(1j * wgc)))), 4))

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.

Activity

  1. slivingston commented on Nov 5, 2026

    @slivingston
    slivingston
    on Nov 5, 2026
    Member
    More actions

    @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

  2. gimballock commented on Nov 5, 2026

    @gimballock
    gimballock
    on Nov 5, 2026
    Author
    More actions

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      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.