Skip to content

Do not report a gain margin at zero frequency for systems with a zero in the origin - #1078

Merged
baggepinnen merged 1 commit into
masterfrom
margin-zero-frequency-gain-margin
Oct 8, 2026
Merged

baggepinnen merged 1 commit into
masterfrom
margin-zero-frequency-gain-margin

Conversation

@baggepinnen

Copy link
Copy Markdown
Member

margin prepends the frequency -0.0 to the frequency grid (_add_zero). This detects crossings of the negative real axis at zero frequency, as in #1045. For a system with more zeros than poles in the origin, the frequency response at ω = 0 is zero, and its computed value is determined by rounding error. When this value is a negative real number, its phase is ±180°. _findCrossings then reports a phase crossover at ω = 0, and sisomargin returns a gain margin 1/|G(0)| of the order 1e15–1e17.

s = tf("s")
G = balreal(ss(s/(s+1)^2))[1]
margin(G; allMargins=true) # master: wgm = [-0.0], gm = [6.5e15]; this PR: wgm = [], gm = [Inf]

This PR removes such crossings. A crossover at ω = 0 is discarded when the system has more zeros than poles in the origin (z = 1 in discrete time). The decision uses integrator_excess, i.e., the same classification as the phase adjustment in sisomargin. For a DelayLtiSystem, every delay equals the identity at ω = 0, so the classification is applied to the delay-free system lft(sys.P.P, I).

System G(0) as computed master: wgm, gm this PR
balreal(ss(s/(s+1)^2))[1] -1.5e-16 [-0.0], [6.5e15] none
balreal(ss(s^2/((s+1)*(s^2 + 0.2s + 4))))[1] -3.0e-17 [-0.0], [8.3e16] none
c2d(ss(s/(s+1)^2), 0.1) -7.1e-16 [-0.0, 31.4], [1.3e15, 20.1] [31.4], [20.1]
balreal(ss(s/(s+1)^2))[1] * delay(0.1) -1.1e-16 [-0.0, 16.9, 78.8], [9.0e15, 17.0, 78.8] [16.9, 78.8], [17.0, 78.8]
-(s+1)/((s+2)*(s+3)) (genuine crossing) -0.17 [-0.0, 1.0], [6.0, 5.0] unchanged

The tests use zpk([1e-17], [-1, -1], 1) and zpk([1 + 1e-15], [0.5, 0.5], 1, 0.1) with and without delays. For these systems the computed G(0) is negative independently of the eigenvalue solver. With the check disabled, 8 of the 9 new assertions fail; the ninth checks the remaining gain margin. The test of #1045 and the new tests with a negative DC gain verify that genuine crossings at ω = 0 are retained.

This problem was found while reviewing #1076, which changes the same function in other lines.

Tests

Linux, Julia 1.13.0, with this branch of ControlSystemsBase developed into each environment:

Package Result
ControlSystemsBase 21352 passed, 6 broken
DyadControlSystems 3483 passed, 2 failed, 1 errored, 20 broken; identical with master (ControlSystemsBase 1.23.0)

DyadControlSystems is the only one of RobustAndOptimalControl, ControlSystemsMTK, ModelPredictiveControl and DyadControlSystems that calls margin. Its failures are an allocation test, a convergence tolerance of the linear MPC, and the MethodError of bodeplot for a ControlSystemIdentification.FRD described in #1077.


Host: demeter2, session 2071354d-b6c5-40db-a9b8-ccbe8a7d8c38, working directory ~/.julia/dev/ControlSystems/.claude/worktrees/joyful-gathering-pumpkin

🤖 Generated with Claude Code

… in the origin

margin evaluates the frequency response at ω = 0 in order to detect
crossings of the negative real axis at zero frequency
(#1045). For a system with more zeros than
poles in the origin, the frequency response at ω = 0 is zero, and its
computed value is determined by rounding error. When this value is a
negative real number, its phase is ±180°, and sisomargin reported a gain
margin at ω = 0 with a magnitude of the order of the reciprocal machine
epsilon.

Such crossings are now removed, using the same classification of zeros
and poles in the origin as integrator_excess. For a DelayLtiSystem, the
classification is applied to the delay-free system, whose frequency
response at ω = 0 coincides with that of the delay system.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@JuliaControlBot

Copy link
Copy Markdown

This is an automated message.
Plots were compared to references. 4/11 images have changed, see differences below.
After pulling this PR, please update the reference images by creating a PR to ControlExamplePlots.jl here.

Difference Reference Image New Image
✔️ 0.0 Reference New
✔️ 0.0 Reference New
✔️ 0.0 Reference New
✔️ 0.0 Reference New

@codecov

codecov Bot commented Oct 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.70%. Comparing base (f1a56a9) to head (42bfff5).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1078      +/-   ##
==========================================
+ Coverage   91.69%   91.70%   +0.01%     
==========================================
  Files          42       42              
  Lines        5789     5796       +7     
==========================================
+ Hits         5308     5315       +7     
  Misses        481      481              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@baggepinnen
baggepinnen merged commit 94c867a into master Oct 8, 2026
6 checks passed
baggepinnen referenced this pull request Oct 8, 2026
Claude-Session: https://claude.ai/code/session_01HigNnun4aJKMoWmY7wyULn

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants