Skip to content

Balance the realization in the frequency-response and norm functions - #1071

Merged
baggepinnen merged 1 commit into
masterfrom
balance-in-freqresp
Sep 18, 2026
Merged

baggepinnen merged 1 commit into
masterfrom
balance-in-freqresp

Conversation

@baggepinnen

Copy link
Copy Markdown
Member

Summary

balance_statespace was called by the frequency-response plot recipes, but not by the functions they call, so a user calling bode, nyquist, margin etc. directly got the unbalanced result. This moves the balancing into the functions themselves, where it belongs, and gives every affected entry point a balance = false keyword argument to turn it off.

The balancing is performed in freqresp/freqresp!, so everything built on top of them benefits. The plot recipes for both Plots.jl and Makie.jl no longer balance the system themselves, they forward their existing balance keyword argument to the inner function.

Entry points that gained a balance keyword argument

freqresp, freqresp!, freqresp_nohess, freqresp_nohess!, bode, bodemag!, nyquist, sigma, margin, sisomargin, delaymargin, dcgain, relative_gain_array, hinfnorm, linfnorm, norm and nicholsplot. All default to balance = true.

evalfr is left unchanged, since it is the single-point primitive that the generic freqresp! loop calls once per element and frequency. Its docstring now states that it does not balance.

Related defects fixed

  • balance_transform computed its working element type as promote_type(eltype(A), eltype(B), eltype(C), Float32), which demotes an integer or rational realization to Float32. Balancing ss([0 1; -1 -1], [0; 1], [1 0], 0) before computing its H-infinity norm therefore lost about eight digits of accuracy. The promotion now goes through float first.
  • balance_statespace returned a StateSpace for every AbstractStateSpace, which turned a HeteroStateSpace holding SMatrix or SizedArray matrices into one holding dense matrices, and additionally did not balance it at all (the in-place branch fails for immutable matrices and the failure was swallowed). A HeteroStateSpace method that computes the transformation with dense matrices and restores the matrix container type is added.

Notes

  • The balance = true default allocates a balanced copy of the system. This is negligible next to the frequency sweep itself, but it does matter for the in-place freqresp! and bodemag!, which are otherwise free of allocations that scale with the state dimension. Both document balance = false for that case.
  • As a side effect of the recipes no longer replacing s with the balanced copy, the Nyquist, sigma and RGA recipes now label the series with the signal names of the original system.
  • docs/src/man/numerical.md no longer states that the user is responsible for balancing before calling bode and nyquist.

Testing

lib/ControlSystemsBase/test/runtests.jl passes in full (21322 passed, 6 broken, 0 failed). New tests in test_freqresp.jl cover the keyword on every entry point and the integer-realization accuracy, and new tests in test_staticsystems.jl cover the container-preserving HeteroStateSpace balancing. The tests in test/ of the outer package cover time responses only and were not run.

🤖 Generated with Claude Code

The functions that depend on a system only through its input-output map now
call `balance_statespace` internally instead of leaving the balancing to the
caller. Balancing improves the numerical conditioning for poorly scaled
realizations and does not change the result, and a `balance = false` keyword
argument is available everywhere to turn it off.

The balancing is performed in `freqresp`/`freqresp!` so that every function
built on top of it benefits. The plot recipes for Plots.jl and Makie.jl no
longer balance the system themselves, they forward their `balance` keyword
argument to the inner function instead. As a side effect, the Nyquist, sigma
and RGA recipes now label the series with the signal names of the original
system rather than those of the balanced copy.

The following entry points gained a `balance` keyword argument: `freqresp`,
`freqresp!`, `freqresp_nohess`, `freqresp_nohess!`, `bode`, `bodemag!`,
`nyquist`, `sigma`, `margin`, `sisomargin`, `delaymargin`, `dcgain`,
`relative_gain_array`, `hinfnorm`, `linfnorm`, `norm` and `nicholsplot`.

Two related defects are fixed:

- `balance_transform` promoted the element type with
  `promote_type(eltype(A), eltype(B), eltype(C), Float32)`, which demotes an
  integer or rational realization to `Float32`. Balancing such a system before
  computing its H-infinity norm lost about eight digits of accuracy.
- `balance_statespace` returned a `StateSpace` for every `AbstractStateSpace`,
  which turned a `HeteroStateSpace` holding static matrices into one holding
  dense matrices. A `HeteroStateSpace` method that restores the matrix
  container type is added.

`evalfr` is left unchanged and its docstring states that it does not balance.

Co-Authored-By: Claude Opus 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 Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.80220% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.64%. Comparing base (77a7a22) to head (ecc7d6f).

Files with missing lines Patch % Lines
lib/ControlSystemsBase/src/freqresp.jl 93.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1071      +/-   ##
==========================================
- Coverage   91.66%   91.64%   -0.02%     
==========================================
  Files          42       42              
  Lines        5793     5795       +2     
==========================================
+ Hits         5310     5311       +1     
- Misses        483      484       +1     

☔ 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 7e1ac64 into master Sep 18, 2026
6 checks passed
@baggepinnen
baggepinnen deleted the balance-in-freqresp branch September 19, 2026 12:30
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