From fa381b52066882fd46a59e3d0be0ece92b9c9bdf Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Mon, 24 Aug 2026 15:54:43 +0000 Subject: [PATCH] =?UTF-8?q?Fix=20CI:=20correct=20H=E2=88=9E=20bound=20and?= =?UTF-8?q?=20de-flake=20the=20scaling-invariance=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ControlSystemsBase 1.21.0 reworked the `hinfnorm` kernel (`_infnorm_two_steps_ct`), and the frequency-weighted-reduction test at test/test_reduction.jl:65 started failing on master as a result. The old bound was calibrated against a wrong value, not against the true norm: CSB 1.20.4: norm(G, Inf) = 0.31417 at ω = Inf CSB 1.21.0: norm(G, Inf) = 0.45826 at ω = 139.44 `G = sysi*(sys - sysr)` has a state dimension of 23 with near-cancelled poles from `fudge_inv` close to the imaginary axis. On that system the old algorithm got stuck at the lower bound `opnorm(D)`, hence the ω = Inf. The peak gain over a 200k-point grid from 1e-4 to 1e4 rad/s is 0.458258 and `hinfnorm2` (via DescriptorSystems, unchanged across the bump) gives 0.458196, so 1.21.0 is the correct value and the bound is raised to 0.5. The reduction itself is unaffected: `norm(sys-sysr, Inf)`, the DC-gain residual property and the comparison against `baltrunc` are identical on both versions. The three scaling-invariance tests compared `hinfnorm2(minreal(sys1-sys2))` against an absolute tolerance. `minreal` occasionally leaves a near-cancelled pole close to the imaginary axis, in which case `ghinfnorm` returns Inf and the test fails; measured at roughly 5% of the random `ssrand(1,1,9)` draws. They now compare the frequency responses on a grid, which is what the section comments already said they did. Worst relative difference over 200 draws: 5.0e-13 (baltrunc2), 1.4e-11 (baltrunc_coprime), 5.0e-13 (baltrunc_unstab), so the 1e-8 tolerance keeps a wide margin while being three orders tighter than the old absolute 1e-5. Full test suite: 1414 passed, 6 broken, 0 failed against CSB 1.21.0. Co-Authored-By: Claude Opus 5 (1M context) --- test/test_reduction.jl | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/test/test_reduction.jl b/test/test_reduction.jl index fd24168f..cb06eefc 100644 --- a/test/test_reduction.jl +++ b/test/test_reduction.jl @@ -62,7 +62,7 @@ sysr, _ = frequency_weighted_reduction(sys, sysi, 1, 3, residual=true) sysr2 = baltrunc(sys, n=3, residual=true)[1] @test sysr.nx == 3 @test norm(sys-sysr, Inf) < 3 -@test norm(sysi*(sys-sysr), Inf) < 0.4 +@test norm(sysi*(sys-sysr), Inf) < 0.5 @test hinorm(minreal(sysi*(sys-sysr))) <= hinorm(minreal(sysi*(sys-sysr2))) @test dcgain(sys)[] ≈ dcgain(sysr)[] rtol=1e-5 # test the residual property @@ -315,22 +315,31 @@ sys_siso = ssrand(1,1,9,proper=true) scaleY = 5.0 scaleU = 0.2 +# The scaled and unscaled reductions are compared on a frequency grid rather than through +# hinfnorm2(minreal(sys1-sys2)): minreal occasionally leaves a near-cancelled pole close to +# the imaginary axis, and the H∞ norm of the difference then comes out as Inf +w_scale = exp10.(LinRange(-3, 3, 500)) +function relative_freqresp_diff(sys1, sys2) + F1 = freqresp(sys1, w_scale) + maximum(abs, F1 - freqresp(sys2, w_scale)) / maximum(abs, F1) +end + # baltrunc2: compare frequency response with and without scaling sysr_noscale, _ = baltrunc2(sys_siso; n=3) sysr_scale, _ = baltrunc2(sys_siso; n=3, scaleY=scaleY, scaleU=scaleU) @test sysr_noscale.nx == sysr_scale.nx == 3 -@test hinfnorm2(minreal(sysr_noscale - sysr_scale))[1] < 1e-5 +@test relative_freqresp_diff(sysr_noscale, sysr_scale) < 1e-8 # baltrunc_coprime: compare frequency response with and without scaling sysr_coprime_noscale, _, _ = baltrunc_coprime(sys_siso; n=3) sysr_coprime_scale, _, _ = baltrunc_coprime(sys_siso; n=3, scaleY=scaleY, scaleU=scaleU) @test sysr_coprime_noscale.nx == sysr_coprime_scale.nx == 3 -@test hinfnorm2(minreal(sysr_coprime_noscale - sysr_coprime_scale))[1] < 1e-5 +@test relative_freqresp_diff(sysr_coprime_noscale, sysr_coprime_scale) < 1e-8 # baltrunc_unstab: compare frequency response with and without scaling sysr_unstab_noscale, _, _ = baltrunc_unstab(sys_siso; n=3) sysr_unstab_scale, _, _ = baltrunc_unstab(sys_siso; n=3, scaleY=scaleY, scaleU=scaleU) @test sysr_unstab_noscale.nx == sysr_unstab_scale.nx == 3 -@test hinfnorm2(minreal(sysr_unstab_noscale - sysr_unstab_scale))[1] < 1e-5 +@test relative_freqresp_diff(sysr_unstab_noscale, sysr_unstab_scale) < 1e-8