Repository navigation
Fix default margin frequency ranges and missing phase guides - #1076
baggepinnen merged 6 commits into
Conversation
baggepinnen
left a comment
There was a problem hiding this comment.
Thank you for the PR. The original example from #771 is resolved by margin and by the Plots recipe for marginplot, and the modified test files pass locally (Linux, Julia 1.13; the allocation test in test_freqresp.jl also passes there). The following issues were found.
1. marginplot throws for systems whose DC gain equals one up to rounding error
The low-frequency extension compares dc != 1 exactly. When the DC gain differs from one only by rounding error, the loop lowers w1 until the magnitude lies on the same side of one as dc, and the extended range then contains crossings of |G| = 1 that are caused by rounding error alone.
Tss = feedback(ss(tf(1, [1, 1, 0])) * ss(pid(1.0, 1.0, 0.1; Tf=0.01)))
margin(Tss; allMargins=true).wpm[] # 19 crossings, 18 of them near 1e-8 rad/s with pm = 180°; master: 1 crossing
marginplot(Tss) # DimensionMismatch at plotting.jl:876
L = ss(tf([1, 1], [1, 10])) * ss(10.0) # unit DC gain
margin(L; allMargins=true).wpm[] # 15 crossings between 1e-8 and 2e-8 rad/sThe exception originates from a defect that is already present on master: when more than five margins are found, the recipe truncates pm and wpm but not fullPhase, so fullPhase .- pm cannot be broadcast. On master the defect is reachable only with an explicit frequency vector (marginplot(Tss, exp10.(range(-8, 4, length=1000))) also throws on master), whereas this PR makes it reachable with the default arguments. Comparing dc and hf against one with a tolerance, and truncating fullPhase together with pm, would address both. The high-frequency check hf != 1 has the same structure.
2. Poles that are located at the origin up to rounding error determine the lower frequency bound
For Val{:margin} the fzp .> -4 filter is removed, so poles that are zero up to rounding error now determine the lower bound of the frequency grid. DemoSystems.double_mass_model() has a pole at -1.1e-14 (a numerical integrator); its margin grid spans 1e-16 to 1e3 rad/s with 1138 points, compared to 1e-2 to 1e3 for the Bode grid, and marginplot displays 20 decades of frequency with a phase transition from 0° to -90° near 1e-14 rad/s.
Outside the range of the poles and zeros the magnitude is monotone, so the gain-based extension already locates crossings below that range; removing the filter is required only for the test case with a resonance at 1e-6 rad/s and gain 1e-13. A criterion that distinguishes eigenvalues located at the origin up to rounding error from slow dynamics would retain both behaviors. count_eigval_multiplicity (used by count_integrators and integrator_excess_with_tol) is the existing utility for this classification, but with its default tolerance eps(maximum(abs, p)) it does not classify the pole at -1.1e-14 as an integrator (count_integrators(double_mass_model()) == 0). A threshold relative to the largest pole magnitude, such as sqrt(eps())*maximum(abs, p) (the tolerance that utility applies for multiplicity two), removes this pole while keeping the resonance at 1e-6 rad/s in the test case. The same classification could replace the exact iszero test that determines the number of integrators and differentiators in the computation of dc.
3. The Makie recipe is not updated
ext/ControlSystemsBaseMakieExt.jl still calls _processfreqplot(Val{:bode}(), ...) (lines 386-387) and draws a phase guide only for each phase margin (lines 491-494). CSMakie.marginplot(tf(1, [1, 13, 40, 0])) therefore still uses a frequency grid that starts at 0.1 rad/s, has the title Pm: [] ωpm: [], and contains no phase guide. Both changes should be applied to the Makie recipe as well, with a corresponding test in test_makie_plots.jl.
4. The new plot tests depend on internals of Plots.jl
The recipe itself uses only RecipesBase, but the tests access p[2].series_list and p[1].series_list[1][:x] (an internal field of Plots.Subplot), as well as the representation into which Plots expands an :hline series (series[:y]) and the stored value of series[:linestyle]. The existing tests in test_plots.jl are smoke tests only, so a change in a Plots release could cause these tests to fail without any change in this package. Extracting the selection of the phase guides (from fullPhase, pm and phasedata) into a helper function, testing that function directly, and calling it from both the Plots and the Makie recipe would remove this dependency and keep the two recipes consistent.
Minor
_default_freq_vector(systems, Val{:margin}())calls itself withVal{:bode}(), sozpkdatais computed twice for each system.- The docstring of
marginplotis not updated to describe the new default frequency range.
|
I'll look into adjusting the default tolerance of |
8813e62 to
2811b2f
Compare
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>
|
Commit 2811b2f addresses the points of the previous review. On top of it, two commits were pushed to this branch:
The full test suite of ControlSystemsBase passes (Linux, Julia 1.13). The remaining gain margin at |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1076 +/- ##
==========================================
+ Coverage 91.73% 91.91% +0.18%
==========================================
Files 42 42
Lines 5865 5935 +70
==========================================
+ Hits 5380 5455 +75
+ Misses 485 480 -5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…vers of structurally zero channels margin(sys) and marginplot used one default frequency vector for all channels and systems. The extension of the frequency range required by a slow pole in one channel was therefore applied to every channel. A channel whose gain equals one at low frequencies then obtained phase crossovers at which rounding error determines whether the gain exceeds one. Each channel now uses its own default frequency vector, so that margin(sys) and margin(sys[i, j]) agree. sisomargin removes the state variables that are structurally uncontrollable or unobservable, as zpkdata does when the default frequency vector is computed. The frequency response of a structurally zero channel is then exactly zero instead of rounding error. Gain crossovers at which the frequency response is zero are removed, since the computed phase of zero is determined by the signs of its real and imaginary parts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lad8HgiT1bm4DFapSoec5U
|
Two further commits were pushed to this branch:
One default frequency vector per channel. s = tf("s")
sys = append(ss(1/((s + a)*(s + 1))), ss(1e4/(s + 1e4)))Number of phase crossovers of channel (2, 2), at which rounding error determines whether the gain exceeds one:
The remaining crossover is located at Structurally zero channels. This problem is also present on
The test set The full test suite of ControlSystemsBase passes (Linux, Julia 1.13): 5773 passed, 6 broken. In a first run, the gang-of-four test at |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HigNnun4aJKMoWmY7wyULn
Closes #771.
The original system,
tf([1], [1, 13, 40, 0]), has a unity-gain crossover near 0.025 rad/s, below the original default grid's 0.1 rad/s lower bound. Select a margin-specific default range using pole/zero features and limiting gains while preserving the original Bode samples. The example now reports a phase margin of 89.53446° at 0.02500054 rad/s in both plotting backends.The limiting-gain checks use type-aware
isapproxcomparisons against unity, so rounding error in unit DC or high-frequency gain does not force extension into noise crossings. Near-origin classification callscount_eigval_multiplicityand adds a relative tolerance locally to margin-grid selection; the shared utility and its defaults are unchanged. Use that classification consistently for features, limiting gains, and endpoint evaluations. This keeps the double-mass model's default range at 0.01–1000 rad/s with 300 points while retaining the 1e-6 resonance. Nonrational delay/interconnected models retain their existing range selection because their augmented channel gains do not define the external limiting gain.Plots and Makie share a backend-independent helper for critical-phase guides, including a fallback when no finite phase margin exists. The helper does not add a margin or a crossover frequency. Keep
fullPhasealigned withpmandwpmwhen Plots retains only five phase margins. Backend tests use stable smoke/regression checks, with guide selection tested directly. Reuse pole/zero features to avoid repeatedzpkdatacalls and document the default range inmarginplot.Explicit frequency vectors are used without extending their range; ordinary Bode grids and public APIs remain unchanged. Discrete default margin grids are capped at Nyquist.
The newly detected discrete-integrator crossover also exposed a pre-existing phase-correction defect: unstable discrete poles were counted using the continuous-time right-half-plane criterion. Use the unit circle for discrete systems, with regressions for an integrator and additional stable/unstable positive poles. This corrects the phase margin from approximately −270° to 90° for the stable integrator case; the same erroneous explicit-grid result was reproduced on untouched master.
Validation with Julia 1.10.12 on Windows (Plots 1.41.7, Makie 0.24.15/CairoMakie 0.15.15):
test_analysis.jl: 226/226 passed.test_plots.jl: 31/31 passed.test_makie_plots.jl: 103/103 passed.test_freqresp.jl: 101 passed, 1 allocation-limit failure at line 69. Untouched master has the identical failure: 123,504 bytes against a 68,006.4-byte limit.Tsstruncation case.git diff --check, and final diff review passed. The full package suite was not run.No package dependencies were added.