Repository navigation
Make the tolerance of the integrator count independent of the time unit - #1077
Merged
Merged
Conversation
…me unit The default radius for multiplicity i was eps(ρ)^(1/i), which is proportional to ρ^(1/i) for i ≥ 2, so the number of integrators depended on the time unit of the model. A backward-stable eigenvalue algorithm perturbs an eigenvalue of multiplicity i by an amount of order ϵ^(1/i) times the scale of the matrix. The default radius is now (100ϵ)^(1/i) * maximum(abs, p), which coincides with the tolerances for simple and double imaginary-axis eigenvalues in the Bruinsma–Steinbuch algorithm. When no multiplicity is found, the returned tolerance is the radius for multiplicity one, since isunstable and sisomargin use it as the threshold on the real part of unstable poles. In integrator_excess, the zeros are counted with the scale of the poles and the zeros. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1077 +/- ##
==========================================
+ Coverage 91.64% 91.69% +0.04%
==========================================
Files 42 42
Lines 5795 5789 -6
==========================================
- Hits 5311 5308 -3
+ Misses 484 481 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
baggepinnen
added a commit
to dongxuelian2/ControlSystems.jl
that referenced
this pull request
Oct 8, 2026
Remove the relative floor sqrt(eps)*scale from _margin_nonintegrators. Since JuliaControl#1077, count_eigval_multiplicity classifies roots that are displaced from the origin by rounding error relative to the scale of the problem, so the floor is no longer required, and it made the classification differ from the one used for the phase adjustment in sisomargin. Classify the zeros relative to the poles as well, as in integrator_excess_with_tol. Previously, a zero in the origin that is not computed exactly, e.g., in a balanced realization of a high-pass system, defined the scale itself, determined the lower bound of the margin grid, and caused margin to report gain margins at frequencies where the phase is determined by rounding error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 8, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
count_eigval_multiplicitydetermines how many computed eigenvalues are located at a given point. It is used bycount_integrators,integrator_excess,isunstable, and the phase adjustment (adjust_phase_start) inmargin,marginplotandbodeplot. The default radius for multiplicityiwaseps(ρ)^(1/i), whereρ = maximum(abs, p). Fori ≥ 2, this radius is proportional toρ^(1/i)rather than toρ, so the result depends on the time unit of the model. Fori = 1, the radiusϵρleaves no margin for the backward error of the eigenvalue computation.This PR changes the default radius to
(100ϵ)^(1/i) * ρ.Motivation of the tolerance
A + Ewith‖E‖ ≲ p(n)ϵ‖A‖. An eigenvalue that belongs to a Jordan block of sizeiis then perturbed by an amount of orderϵ^(1/i)‖A‖, i.e., proportionally to the scale ofA. The radius must therefore scale linearly with the magnitude of the eigenvalues.ρis used in place of‖A‖since only the eigenvalues are available (ρ ≤ ‖A‖).100ϵand10√ϵ = (100ϵ)^(1/2)are the tolerances for simple and double imaginary-axis eigenvalues in the Bruinsma–Steinbuch algorithm for theH∞norm, as implemented in DescriptorSystems.jl (norminfc/norminfd) and in SLICOT AB13DD. DescriptorSystems.jl and MatrixPencils.jl make other structural decisions, such as multiplicities of infinite eigenvalues, by rank decisions with the relative tolerancenϵ; that constant performed worse in the comparison below. MatrixEquations.jl contains no tolerance of this kind.Further changes
ϵis the machine epsilon of the element type of the eigenvalues, so that models withFloat32coefficients are handled.<=the radius, so that eigenvalues located exactly at the point are counted also when the scale is zero.n.isunstableandsisomarginuse the returned tolerance as the threshold on the real part of unstable poles. With the radius for multiplicityn(0.033for a system with state dimension 10),isunstableclassified a system with a pole at+0.01as not unstable.integrator_excess, the zeros are counted with the scalemax(maximum(abs, p), maximum(abs, z)). The zeros are computed from the system pencil, whose scale is at least that of the poles, and a single zero in the origin does not provide a scale of its own.eretains its meaning (radiuse^(1/i)).Examples
1/(s²(s+1)(s+2))after an orthogonal similarity transformation, time unit scaled by1e4-1e-3·(1:10), no integratormarginreports a phase margin of -703.6° (16.4° when the time unit is scaled by 1000)DemoSystems.double_mass_model()(the pole in the origin is computed as-1.1e-14)s/(s+1)²after a rotation (the zero is computed as2.3e-16)isunstablewith a pole at+0.01, state dimension 10falsetrueA comparison on 2000 random realizations per case (modal form with one Jordan block of size
mat the origin, 2–8 additional stable poles spread over three decades, an orthogonal similarity transformation, and a random time scale in[1e-4, 1e4]) gave the following numbers of wrong counts:m(ϵ)^(1/i)ρ(nϵ)^(1/i)ρ(100ϵ)^(1/i)ρ(this PR)Two simple poles closer to the origin than approximately
1e-8 ρcannot be distinguished from a perturbed double integrator by any tolerance of this form; this ambiguity is inherent. The existing test withpade(OL, k), which compares the count with aBigFloatcomputation, passes for allk.The problem was found while reviewing #1076, where a pole of
DemoSystems.double_mass_model()that is zero up to rounding error extended the frequency range ofmarginplot.Tests
All test suites were run with Julia 1.13.0 on Linux, with this branch of ControlSystemsBase developed into each environment. The downstream packages were tested at their current default branches.
The failures in RobustAndOptimalControl and DyadControlSystems occur identically with master and do not involve the functions changed here:
test_uncertainty.jl:42:rand(Δ(2, δc), 100)returns aDiagonalrather than aMatrixon Julia 1.13.test/MPC/test_collocation.jl:140: an allocation test (160 bytes).test/MPC/test_quadtank.jl:209: a convergence tolerance of the linear MPC; the final state values are bit-identical with master.bodeplotof aControlSystemIdentification.FRDthrows aMethodError, sincebodeplotpasses the keywordbalance(added in Balance the realization in the frequency-response and norm functions #1071) tobode(::FRD, w; unwrap), which does not accept it.Host:
demeter2, session2071354d-b6c5-40db-a9b8-ccbe8a7d8c38, working directory~/.julia/dev/ControlSystems/.claude/worktrees/joyful-gathering-pumpkin🤖 Generated with Claude Code