From f1d97e0d240d59e5a44ee109b04fe70eac84d233 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Fri, 18 Sep 2026 05:27:46 +0000 Subject: [PATCH 1/2] Forward keyword arguments to the inner DescriptorSystems methods `baltrunc_unstab` splatted its keyword arguments into both `DescriptorSystems.gsdec` and `DescriptorSystems.gbalmr`, neither of which slurps keyword arguments. Any keyword argument specific to one of the two therefore raised a `MethodError`: `smarg`, which determines the stability boundary used for the additive decomposition, could not be passed at all, and neither could `atolhsv` and the other truncation tolerances. `baltrunc_coprime` forwarded its keyword arguments to `gbalmr` only, so the factorization performed by `factorization` could not be controlled. Keyword arguments are now distributed over the inner methods: each inner method receives the keyword arguments that appear in the keyword-argument list of its own methods, so tolerances such as `atol` and `rtol` reach all of them while `smarg` reaches `gsdec` and the coprime factorization, and `atolhsv` reaches `gbalmr`. The keyword-argument lists are obtained from `Base.kwarg_decl`; `glcf` and `glcfid`, which slurp their keyword arguments, are resolved through `grcf` and `grcfid`. A keyword argument accepted by none of the inner methods of a given wrapper now raises an `ArgumentError` listing the supported names instead of being silently ignored. The keyword arguments `job`, `ord` and `matchdc` are determined by the arguments of the wrappers (`n` and `residual`) and are no longer accepted. Co-Authored-By: Claude Opus 5 (1M context) --- Project.toml | 2 +- src/descriptor.jl | 67 +++++++++++++++++++++++++++++++++++++---- test/test_descriptor.jl | 36 ++++++++++++++++++++++ 3 files changed, 98 insertions(+), 7 deletions(-) diff --git a/Project.toml b/Project.toml index 539a7802..05cd3812 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "RobustAndOptimalControl" uuid = "21fd56a4-db03-40ee-82ee-a87907bee541" authors = ["Fredrik Bagge Carlson", "Marcus Greiff"] -version = "0.4.52" +version = "0.4.53" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" diff --git a/src/descriptor.jl b/src/descriptor.jl index 48a1af6d..8698221b 100644 --- a/src/descriptor.jl +++ b/src/descriptor.jl @@ -79,6 +79,55 @@ const νgap = nugap +# `glcf` and `glcfid` slurp their keyword arguments and forward them to `grcf` and `grcfid`, the keyword-argument lists of which are thus used in their place. +const _KWARG_PROXY = IdDict{Any,Any}( + DescriptorSystems.glcf => DescriptorSystems.grcf, + DescriptorSystems.glcfid => DescriptorSystems.grcfid, +) + +# Keyword arguments that are determined by the arguments of the wrapper functions below and are therefore never forwarded +const _RESERVED_KWARGS = (:job, :ord, :matchdc) + +""" + _kwarg_names(f) + +The set of keyword-argument names accepted by the methods of `f`, excluding `_RESERVED_KWARGS`. A function that slurps its keyword arguments and has no entry in `_KWARG_PROXY` is reported as accepting `Symbol("kwargs...")`, i.e., any keyword argument. +""" +function _kwarg_names(f) + f = get(_KWARG_PROXY, f, f) + names = Set{Symbol}() + for m in methods(f) + union!(names, Base.kwarg_decl(m)) + end + setdiff!(names, _RESERVED_KWARGS) +end + +""" + _select_kwargs(f, kwargs) + +The subset of `kwargs` that is accepted by `f`, for forwarding to `f`. See [`_kwarg_names`](@ref). +""" +function _select_kwargs(f, kwargs) + names = _kwarg_names(f) + if Symbol("kwargs...") ∈ names + return NamedTuple(k => v for (k, v) in pairs(kwargs) if k ∉ _RESERVED_KWARGS) + end + NamedTuple(k => v for (k, v) in pairs(kwargs) if k ∈ names) +end + +""" + _check_kwargs(kwargs, fs...) + +Throw an `ArgumentError` if any of `kwargs` is accepted by none of the functions `fs`, in which case it would be silently ignored by [`_select_kwargs`](@ref). +""" +function _check_kwargs(kwargs, fs...) + accepted = union(_kwarg_names.(fs)...) + Symbol("kwargs...") ∈ accepted && return + unsupported = filter(∉(accepted), collect(keys(kwargs))) + isempty(unsupported) && return + throw(ArgumentError("Unsupported keyword argument(s): $(join(unsupported, ", ")). Keyword arguments are forwarded to $(join(fs, ", ")), which accept $(join(sort!(collect(accepted)), ", ")).")) +end + """ sysr, hs = baltrunc2(sys::LTISystem; residual=false, n=missing, kwargs...) @@ -88,11 +137,12 @@ For keyword arguments, see the docstring of `DescriptorSystems.gbalmr`, reproduc $(@doc(DescriptorSystems.gbalmr)) """ function baltrunc2(sys::LTISystem; residual=false, n=missing, scaleY=1.0, scaleU=1.0, kwargs...) + _check_kwargs(kwargs, DescriptorSystems.gbalmr) # Apply scaling if needed A, B, C, D = ssdata(sys) # Divide by scaling factors to normalize to ~[-1,1] sys_scaled = ss(A, B / scaleU, C / scaleY, scaleY \ D / scaleU, sys.timeevol) - sysr, hs = DescriptorSystems.gbalmr(dss(sys_scaled); matchdc=residual, ord=n, kwargs...) + sysr, hs = DescriptorSystems.gbalmr(dss(sys_scaled); matchdc=residual, ord=n, _select_kwargs(DescriptorSystems.gbalmr, kwargs)...) # Multiply by scaling factors to restore original units Ar, Br, Cr, Dr = ssdata(ss(sysr)) sys_final = ss(Ar, Br * scaleU, scaleY * Cr, scaleY * Dr * scaleU, sys.timeevol) @@ -109,10 +159,11 @@ Coprime-factor reduction performs a coprime factorization of the model into \$P( # Arguments: - `factorization`: The function to perform the coprime factorization. A non-normalized factorization may be used by passing `RobustAndOptimalControl.DescriptorSystems.glcf`. -- `kwargs`: Are passed to `DescriptorSystems.gbalmr`, the docstring of which is reproduced below: +- `kwargs`: Are forwarded to `DescriptorSystems.gbalmr` and to `factorization`, each of which receives the keyword arguments appearing in its own keyword-argument list. Tolerances such as `atol` and `rtol` are thus passed to both, while, e.g., `atolhsv` is passed to `gbalmr` only and `smarg` (supported by `RobustAndOptimalControl.DescriptorSystems.glcf`) to the factorization only. A keyword argument accepted by neither results in an `ArgumentError`. The docstring of `DescriptorSystems.gbalmr` is reproduced below: $(@doc(DescriptorSystems.gbalmr)) """ function baltrunc_coprime(sys, info=nothing; residual=false, n=missing, factorization::F = DescriptorSystems.gnlcf, scaleY=1.0, scaleU=1.0, kwargs...) where F + _check_kwargs(kwargs, factorization, DescriptorSystems.gbalmr) # Apply scaling if needed A, B, C, D = ssdata(sys) # Divide by scaling factors to normalize to ~[-1,1] @@ -120,11 +171,11 @@ function baltrunc_coprime(sys, info=nothing; residual=false, n=missing, factoriz if info !== nothing && hasproperty(info, :NM) @unpack N, M, NM = info else - N,M = factorization(dss(sys_scaled)) + N,M = factorization(dss(sys_scaled); _select_kwargs(factorization, kwargs)...) A,E,B,C,D = DescriptorSystems.dssdata(N) NM = DescriptorSystems.dss(A,E,[B M.B],C,[D M.D]) end - NMr, hs = DescriptorSystems.gbalmr(NM; matchdc=residual, ord=n, kwargs...) + NMr, hs = DescriptorSystems.gbalmr(NM; matchdc=residual, ord=n, _select_kwargs(DescriptorSystems.gbalmr, kwargs)...) A,E,B,C,D = DescriptorSystems.dssdata(DescriptorSystems.dss2ss(NMr)[1]) @@ -151,8 +202,12 @@ end Balanced truncation for unstable models. An additive decomposition of sys into `sys = sys_stable + sys_unstable` is performed after which `sys_stable` is reduced. The order `n` must not be less than the number of unstable poles. See `baltrunc2` for other keyword arguments. + +Keyword arguments are forwarded to `DescriptorSystems.gsdec`, which performs the additive decomposition, and to `DescriptorSystems.gbalmr`, which reduces the stable part. Each of the two receives the keyword arguments appearing in its own keyword-argument list. Tolerances such as `atol` and `rtol` are thus passed to both, while, e.g., `smarg`, which determines the stability boundary used for the decomposition, is passed to `gsdec` only and `atolhsv` to `gbalmr` only. A keyword argument accepted by neither results in an `ArgumentError`. The docstring of `DescriptorSystems.gsdec` is reproduced below: +$(@doc(DescriptorSystems.gsdec)) """ function baltrunc_unstab(sys::LTISystem, info=nothing; residual=false, n=missing, scaleY=1.0, scaleU=1.0, kwargs...) + _check_kwargs(kwargs, DescriptorSystems.gsdec, DescriptorSystems.gbalmr) # Apply scaling if needed A, B, C, D = ssdata(sys) # Divide by scaling factors to normalize to ~[-1,1] @@ -160,13 +215,13 @@ function baltrunc_unstab(sys::LTISystem, info=nothing; residual=false, n=missing if info !== nothing && hasproperty(info, :stab) @unpack stab, unstab = info else - stab, unstab = DescriptorSystems.gsdec(dss(sys_scaled); job="stable", kwargs...) + stab, unstab = DescriptorSystems.gsdec(dss(sys_scaled); job="stable", _select_kwargs(DescriptorSystems.gsdec, kwargs)...) end nx_unstab = size(unstab.A, 1) if n isa Integer && n < nx_unstab error("The model contains $(nx_unstab) poles outside the stability region, the reduced-order model must be of at least this order.") end - sysr, hs = DescriptorSystems.gbalmr(stab; matchdc=residual, ord=n-nx_unstab, kwargs...) + sysr, hs = DescriptorSystems.gbalmr(stab; matchdc=residual, ord=n-nx_unstab, _select_kwargs(DescriptorSystems.gbalmr, kwargs)...) # Multiply by scaling factors to restore original units Ar, Br, Cr, Dr = ssdata(ss(sysr + unstab)) sys_final = ss(Ar, Br * scaleU, scaleY * Cr, scaleY * Dr * scaleU, sys.timeevol) diff --git a/test/test_descriptor.jl b/test/test_descriptor.jl index 243b1ad4..7b871e61 100644 --- a/test/test_descriptor.jl +++ b/test/test_descriptor.jl @@ -59,3 +59,39 @@ end >= 94 # bodeplot([sys, sysr]) end + +using RobustAndOptimalControl: baltrunc2, baltrunc_coprime, baltrunc_unstab, _kwarg_names, _select_kwargs +using RobustAndOptimalControl.DescriptorSystems: gsdec, gbalmr, glcf + +@testset "keyword-argument forwarding" begin + @info "Testing keyword-argument forwarding" + + @test _kwarg_names(gsdec) == Set([:prescale, :smarg, :fast, :atol, :atol1, :atol2, :rtol]) + @test :smarg ∈ _kwarg_names(glcf) # glcf slurps its keyword arguments into grcf + @test :ord ∉ _kwarg_names(gbalmr) # determined by the argument n + @test _select_kwargs(gsdec, (; smarg=-0.1, atolhsv=1e-3, atol=1e-9)) === (; smarg=-0.1, atol=1e-9) + @test _select_kwargs(gbalmr, (; smarg=-0.1, atolhsv=1e-3, atol=1e-9)) === (; atolhsv=1e-3, atol=1e-9) + + sys = ssrand(2,3,20, stable=true) + sysus = ssrand(2,3,2, stable=true) + sysus.A .*= -1 + sysu = sys + sysus + + # smarg is accepted by gsdec only, atolhsv by gbalmr only + sysr, _ = baltrunc_unstab(sysu; n=12, smarg=-0.01, atolhsv=1e-8) + @test sysr.nx <= 12 + @test_throws ArgumentError baltrunc_unstab(sysu; n=12, smrag=-0.01) + + # smarg is accepted by glcf but not by the default factorization gnlcf + sysr, _ = baltrunc_coprime(sysu; n=12, factorization=glcf, smarg=-0.01, atolhsv=1e-8) + @test sysr.nx <= 12 + @test_throws ArgumentError baltrunc_coprime(sysu; n=12, smarg=-0.01) + @test_throws ArgumentError baltrunc2(sys; n=12, smarg=-0.01) + + # smarg reaches the additive decomposition: poles to the right of smarg are treated as unstable and are preserved + _, unstab = gsdec(dss(sysu); job="stable", smarg=-0.5) + nx_unstab = size(unstab.A, 1) + @test nx_unstab == count(p -> real(p) > -0.5, poles(sysu)) + sysr, _ = baltrunc_unstab(sysu; n=nx_unstab, smarg=-0.5) + @test sysr.nx == nx_unstab +end From 6bfe2eea384004b2cb19a67ed70a774c61192a78 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Fri, 18 Sep 2026 05:55:51 +0000 Subject: [PATCH 2/2] List the forwarded keyword arguments explicitly Replace the keyword-argument introspection (`_kwarg_names`, `_select_kwargs`, `_check_kwargs` and the associated tables) with explicit keyword arguments in the signatures of the wrappers: - `baltrunc_unstab` accepts `smarg`, which is passed to `DescriptorSystems.gsdec`, and `fast, atol, atol1, atol2, rtol`, which are passed to both `gsdec` and `DescriptorSystems.gbalmr`. The remaining keyword arguments are passed to `gbalmr`. - `baltrunc_coprime` accepts `factorization_kwargs`, a named tuple passed to `factorization` only, and `fast, atol, atol1, atol2, rtol`, which are passed to both `factorization` and `gbalmr`. The remaining keyword arguments are passed to `gbalmr`. - `baltrunc2` is restored to its previous form, all of its keyword arguments are passed to `gbalmr`. The new keyword arguments are documented in the docstrings. Co-Authored-By: Claude Opus 5 (1M context) --- src/descriptor.jl | 84 ++++++++++------------------------------- test/test_descriptor.jl | 17 ++------- 2 files changed, 24 insertions(+), 77 deletions(-) diff --git a/src/descriptor.jl b/src/descriptor.jl index 8698221b..6f3a3e22 100644 --- a/src/descriptor.jl +++ b/src/descriptor.jl @@ -79,55 +79,6 @@ const νgap = nugap -# `glcf` and `glcfid` slurp their keyword arguments and forward them to `grcf` and `grcfid`, the keyword-argument lists of which are thus used in their place. -const _KWARG_PROXY = IdDict{Any,Any}( - DescriptorSystems.glcf => DescriptorSystems.grcf, - DescriptorSystems.glcfid => DescriptorSystems.grcfid, -) - -# Keyword arguments that are determined by the arguments of the wrapper functions below and are therefore never forwarded -const _RESERVED_KWARGS = (:job, :ord, :matchdc) - -""" - _kwarg_names(f) - -The set of keyword-argument names accepted by the methods of `f`, excluding `_RESERVED_KWARGS`. A function that slurps its keyword arguments and has no entry in `_KWARG_PROXY` is reported as accepting `Symbol("kwargs...")`, i.e., any keyword argument. -""" -function _kwarg_names(f) - f = get(_KWARG_PROXY, f, f) - names = Set{Symbol}() - for m in methods(f) - union!(names, Base.kwarg_decl(m)) - end - setdiff!(names, _RESERVED_KWARGS) -end - -""" - _select_kwargs(f, kwargs) - -The subset of `kwargs` that is accepted by `f`, for forwarding to `f`. See [`_kwarg_names`](@ref). -""" -function _select_kwargs(f, kwargs) - names = _kwarg_names(f) - if Symbol("kwargs...") ∈ names - return NamedTuple(k => v for (k, v) in pairs(kwargs) if k ∉ _RESERVED_KWARGS) - end - NamedTuple(k => v for (k, v) in pairs(kwargs) if k ∈ names) -end - -""" - _check_kwargs(kwargs, fs...) - -Throw an `ArgumentError` if any of `kwargs` is accepted by none of the functions `fs`, in which case it would be silently ignored by [`_select_kwargs`](@ref). -""" -function _check_kwargs(kwargs, fs...) - accepted = union(_kwarg_names.(fs)...) - Symbol("kwargs...") ∈ accepted && return - unsupported = filter(∉(accepted), collect(keys(kwargs))) - isempty(unsupported) && return - throw(ArgumentError("Unsupported keyword argument(s): $(join(unsupported, ", ")). Keyword arguments are forwarded to $(join(fs, ", ")), which accept $(join(sort!(collect(accepted)), ", ")).")) -end - """ sysr, hs = baltrunc2(sys::LTISystem; residual=false, n=missing, kwargs...) @@ -137,12 +88,11 @@ For keyword arguments, see the docstring of `DescriptorSystems.gbalmr`, reproduc $(@doc(DescriptorSystems.gbalmr)) """ function baltrunc2(sys::LTISystem; residual=false, n=missing, scaleY=1.0, scaleU=1.0, kwargs...) - _check_kwargs(kwargs, DescriptorSystems.gbalmr) # Apply scaling if needed A, B, C, D = ssdata(sys) # Divide by scaling factors to normalize to ~[-1,1] sys_scaled = ss(A, B / scaleU, C / scaleY, scaleY \ D / scaleU, sys.timeevol) - sysr, hs = DescriptorSystems.gbalmr(dss(sys_scaled); matchdc=residual, ord=n, _select_kwargs(DescriptorSystems.gbalmr, kwargs)...) + sysr, hs = DescriptorSystems.gbalmr(dss(sys_scaled); matchdc=residual, ord=n, kwargs...) # Multiply by scaling factors to restore original units Ar, Br, Cr, Dr = ssdata(ss(sysr)) sys_final = ss(Ar, Br * scaleU, scaleY * Cr, scaleY * Dr * scaleU, sys.timeevol) @@ -150,7 +100,7 @@ function baltrunc2(sys::LTISystem; residual=false, n=missing, scaleY=1.0, scaleU end """ - sysr, hs, info = baltrunc_coprime(sys; residual = false, n = missing, factorization::F = DescriptorSystems.gnlcf, kwargs...) + sysr, hs, info = baltrunc_coprime(sys; residual = false, n = missing, factorization::F = DescriptorSystems.gnlcf, factorization_kwargs = (;), kwargs...) Compute a balanced truncation of the left coprime factorization of `sys`. See [`baltrunc2`](@ref) for additional keyword-argument help. @@ -159,11 +109,14 @@ Coprime-factor reduction performs a coprime factorization of the model into \$P( # Arguments: - `factorization`: The function to perform the coprime factorization. A non-normalized factorization may be used by passing `RobustAndOptimalControl.DescriptorSystems.glcf`. -- `kwargs`: Are forwarded to `DescriptorSystems.gbalmr` and to `factorization`, each of which receives the keyword arguments appearing in its own keyword-argument list. Tolerances such as `atol` and `rtol` are thus passed to both, while, e.g., `atolhsv` is passed to `gbalmr` only and `smarg` (supported by `RobustAndOptimalControl.DescriptorSystems.glcf`) to the factorization only. A keyword argument accepted by neither results in an `ArgumentError`. The docstring of `DescriptorSystems.gbalmr` is reproduced below: +- `factorization_kwargs`: A named tuple of keyword arguments passed to `factorization` only, e.g., `factorization_kwargs = (; smarg = -0.1)` to select the stability margin of `DescriptorSystems.glcf`. +- `fast, atol, atol1, atol2, rtol`: Tolerances and algorithm options passed to both `factorization` and `DescriptorSystems.gbalmr`. +- `kwargs`: Are passed to `DescriptorSystems.gbalmr`, the docstring of which is reproduced below: $(@doc(DescriptorSystems.gbalmr)) """ -function baltrunc_coprime(sys, info=nothing; residual=false, n=missing, factorization::F = DescriptorSystems.gnlcf, scaleY=1.0, scaleU=1.0, kwargs...) where F - _check_kwargs(kwargs, factorization, DescriptorSystems.gbalmr) +function baltrunc_coprime(sys, info=nothing; residual=false, n=missing, factorization::F = DescriptorSystems.gnlcf, factorization_kwargs = (;), scaleY=1.0, scaleU=1.0, + fast = true, atol = 0.0, atol1 = atol, atol2 = atol, + rtol = sys.nx*eps(real(float(one(numeric_type(sys)))))*iszero(min(atol1, atol2)), kwargs...) where F # Apply scaling if needed A, B, C, D = ssdata(sys) # Divide by scaling factors to normalize to ~[-1,1] @@ -171,11 +124,11 @@ function baltrunc_coprime(sys, info=nothing; residual=false, n=missing, factoriz if info !== nothing && hasproperty(info, :NM) @unpack N, M, NM = info else - N,M = factorization(dss(sys_scaled); _select_kwargs(factorization, kwargs)...) + N,M = factorization(dss(sys_scaled); fast, atol1, atol2, rtol, factorization_kwargs...) A,E,B,C,D = DescriptorSystems.dssdata(N) NM = DescriptorSystems.dss(A,E,[B M.B],C,[D M.D]) end - NMr, hs = DescriptorSystems.gbalmr(NM; matchdc=residual, ord=n, _select_kwargs(DescriptorSystems.gbalmr, kwargs)...) + NMr, hs = DescriptorSystems.gbalmr(NM; matchdc=residual, ord=n, fast, atol1, atol2, rtol, kwargs...) A,E,B,C,D = DescriptorSystems.dssdata(DescriptorSystems.dss2ss(NMr)[1]) @@ -197,17 +150,20 @@ end """ - baltrunc_unstab(sys::LTISystem; residual = false, n = missing, kwargs...) + baltrunc_unstab(sys::LTISystem; residual = false, n = missing, smarg = missing, kwargs...) Balanced truncation for unstable models. An additive decomposition of sys into `sys = sys_stable + sys_unstable` is performed after which `sys_stable` is reduced. The order `n` must not be less than the number of unstable poles. See `baltrunc2` for other keyword arguments. -Keyword arguments are forwarded to `DescriptorSystems.gsdec`, which performs the additive decomposition, and to `DescriptorSystems.gbalmr`, which reduces the stable part. Each of the two receives the keyword arguments appearing in its own keyword-argument list. Tolerances such as `atol` and `rtol` are thus passed to both, while, e.g., `smarg`, which determines the stability boundary used for the decomposition, is passed to `gsdec` only and `atolhsv` to `gbalmr` only. A keyword argument accepted by neither results in an `ArgumentError`. The docstring of `DescriptorSystems.gsdec` is reproduced below: -$(@doc(DescriptorSystems.gsdec)) +# Arguments: +- `smarg`: The stability margin used by `DescriptorSystems.gsdec` to perform the additive decomposition, i.e., the boundary of the stability region. Poles to the right of `smarg` (outside the circle of radius `smarg` for discrete-time models) end up in `sys_unstable` and are preserved by the reduction. Defaults to `-sqrt(eps())` in continuous time and `1-sqrt(eps())` in discrete time. +- `fast, atol, atol1, atol2, rtol`: Tolerances and algorithm options passed to both `DescriptorSystems.gsdec` and `DescriptorSystems.gbalmr`. +- `kwargs`: Are passed to `DescriptorSystems.gbalmr`. """ -function baltrunc_unstab(sys::LTISystem, info=nothing; residual=false, n=missing, scaleY=1.0, scaleU=1.0, kwargs...) - _check_kwargs(kwargs, DescriptorSystems.gsdec, DescriptorSystems.gbalmr) +function baltrunc_unstab(sys::LTISystem, info=nothing; residual=false, n=missing, scaleY=1.0, scaleU=1.0, + smarg = missing, fast = true, atol = 0.0, atol1 = atol, atol2 = atol, + rtol = sys.nx*eps(real(float(one(numeric_type(sys)))))*iszero(min(atol1, atol2)), kwargs...) # Apply scaling if needed A, B, C, D = ssdata(sys) # Divide by scaling factors to normalize to ~[-1,1] @@ -215,13 +171,13 @@ function baltrunc_unstab(sys::LTISystem, info=nothing; residual=false, n=missing if info !== nothing && hasproperty(info, :stab) @unpack stab, unstab = info else - stab, unstab = DescriptorSystems.gsdec(dss(sys_scaled); job="stable", _select_kwargs(DescriptorSystems.gsdec, kwargs)...) + stab, unstab = DescriptorSystems.gsdec(dss(sys_scaled); job="stable", smarg, fast, atol1, atol2, rtol) end nx_unstab = size(unstab.A, 1) if n isa Integer && n < nx_unstab error("The model contains $(nx_unstab) poles outside the stability region, the reduced-order model must be of at least this order.") end - sysr, hs = DescriptorSystems.gbalmr(stab; matchdc=residual, ord=n-nx_unstab, _select_kwargs(DescriptorSystems.gbalmr, kwargs)...) + sysr, hs = DescriptorSystems.gbalmr(stab; matchdc=residual, ord=n-nx_unstab, fast, atol1, atol2, rtol, kwargs...) # Multiply by scaling factors to restore original units Ar, Br, Cr, Dr = ssdata(ss(sysr + unstab)) sys_final = ss(Ar, Br * scaleU, scaleY * Cr, scaleY * Dr * scaleU, sys.timeevol) diff --git a/test/test_descriptor.jl b/test/test_descriptor.jl index 7b871e61..830d7ffb 100644 --- a/test/test_descriptor.jl +++ b/test/test_descriptor.jl @@ -60,33 +60,24 @@ end >= 94 end -using RobustAndOptimalControl: baltrunc2, baltrunc_coprime, baltrunc_unstab, _kwarg_names, _select_kwargs -using RobustAndOptimalControl.DescriptorSystems: gsdec, gbalmr, glcf +using RobustAndOptimalControl: baltrunc2, baltrunc_coprime, baltrunc_unstab +using RobustAndOptimalControl.DescriptorSystems: gsdec, glcf @testset "keyword-argument forwarding" begin @info "Testing keyword-argument forwarding" - @test _kwarg_names(gsdec) == Set([:prescale, :smarg, :fast, :atol, :atol1, :atol2, :rtol]) - @test :smarg ∈ _kwarg_names(glcf) # glcf slurps its keyword arguments into grcf - @test :ord ∉ _kwarg_names(gbalmr) # determined by the argument n - @test _select_kwargs(gsdec, (; smarg=-0.1, atolhsv=1e-3, atol=1e-9)) === (; smarg=-0.1, atol=1e-9) - @test _select_kwargs(gbalmr, (; smarg=-0.1, atolhsv=1e-3, atol=1e-9)) === (; atolhsv=1e-3, atol=1e-9) - sys = ssrand(2,3,20, stable=true) sysus = ssrand(2,3,2, stable=true) sysus.A .*= -1 sysu = sys + sysus - # smarg is accepted by gsdec only, atolhsv by gbalmr only + # smarg is accepted by gsdec, atolhsv by gbalmr sysr, _ = baltrunc_unstab(sysu; n=12, smarg=-0.01, atolhsv=1e-8) @test sysr.nx <= 12 - @test_throws ArgumentError baltrunc_unstab(sysu; n=12, smrag=-0.01) # smarg is accepted by glcf but not by the default factorization gnlcf - sysr, _ = baltrunc_coprime(sysu; n=12, factorization=glcf, smarg=-0.01, atolhsv=1e-8) + sysr, _ = baltrunc_coprime(sysu; n=12, factorization=glcf, factorization_kwargs=(; smarg=-0.01), atolhsv=1e-8) @test sysr.nx <= 12 - @test_throws ArgumentError baltrunc_coprime(sysu; n=12, smarg=-0.01) - @test_throws ArgumentError baltrunc2(sys; n=12, smarg=-0.01) # smarg reaches the additive decomposition: poles to the right of smarg are treated as unstable and are preserved _, unstab = gsdec(dss(sysu); job="stable", smarg=-0.5)