Skip to content

Commit 948a6b4

Browse files
authored
Merge pull request #976 from JuliaControl/bigfloatzeros
enable `tzeros` for `BigFloat`
2 parents 5c6f583 + 88f3187 commit 948a6b4

File tree

5 files changed

+138
-10
lines changed

5 files changed

+138
-10
lines changed

lib/ControlSystemsBase/Project.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66"
5151
DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2"
5252
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
5353
FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000"
54-
GenericLinearAlgebra = "14197337-ba66-59df-a3e3-ca00e7dcff7a"
54+
GenericSchur = "c145ed77-6b09-5dd9-b285-bf645a82121e"
5555
GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71"
5656
ImplicitDifferentiation = "57b37032-215b-411a-8a7c-41a003a55207"
5757
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
@@ -60,4 +60,4 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
6060
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
6161

6262
[targets]
63-
test = ["Test", "Aqua", "ComponentArrays", "Documenter", "DSP", "FiniteDifferences", "ImplicitDifferentiation", "GenericLinearAlgebra", "GR", "Plots", "SparseArrays", "StaticArrays"]
63+
test = ["Test", "Aqua", "ComponentArrays", "Documenter", "DSP", "FiniteDifferences", "ImplicitDifferentiation", "GenericSchur", "GR", "Plots", "SparseArrays", "StaticArrays"]

lib/ControlSystemsBase/src/ControlSystemsBase.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ function __init__()
242242
printstyled(io, "install and load ControlSystems.jl, or pass the keyword method = :zoh", color=:green, bold=true)
243243
print(io, " for automatic discretization (applicable to systems without delays or nonlinearities only).")
244244
elseif exc.f (eigvals!, ) && argtypes[1] <: AbstractMatrix{<:Number}
245-
printstyled(io, "\nComputing eigenvalues of a matrix with exotic element types may require `using GenericLinearAlgebra`.", color=:green, bold=true)
245+
printstyled(io, "\nComputing eigenvalues of a matrix with exotic element types may require `using GenericSchur`.", color=:green, bold=true)
246246
end
247247
plots_id = Base.PkgId(UUID("91a5bcdd-55d7-5caf-9e0b-520d859cae80"), "Plots")
248248
if exc.f isa Function && nameof(exc.f) === :plot && parentmodule(argtypes[1]) == @__MODULE__() && !haskey(Base.loaded_modules, plots_id)

lib/ControlSystemsBase/src/analysis.jl

+114-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
Compute the poles of system `sys`.
55
66
Note: Poles with multiplicity `n > 1` may suffer numerical inaccuracies on the order `eps(numeric_type(sys))^(1/n)`, i.e., a double pole in a system with `Float64` coefficients may be computed with an error of about `√(eps(Float64)) ≈ 1.5e-8`.
7+
8+
To compute the poles of a system with non-BLAS floats, such as `BigFloat`, install and load the package `GenericSchur.jl` before calling `poles`.
79
"""
810
poles(sys::AbstractStateSpace) = eigvalsnosort(sys.A)
911

@@ -243,6 +245,8 @@ Compute the invariant zeros of the system `sys`. If `sys` is a minimal
243245
realization, these are also the transmission zeros.
244246
245247
If `sys` is a state-space system the function has additional keyword arguments, see [`?ControlSystemsBase.MatrixPencils.spzeros`](https://andreasvarga.github.io/MatrixPencils.jl/dev/sklfapps.html#MatrixPencils.spzeros) for more details. If `extra = Val(true)`, the function returns `z, iz, KRInfo` where `z` are the transmission zeros, information on the multiplicities of infinite zeros in `iz` and information on the Kronecker-structure in the KRInfo object. The number of infinite zeros is the sum of the components of iz.
248+
249+
To compute zeros of a system with non-BLAS floats, such as `BigFloat`, install and load the package `GenericSchur.jl` before calling `tzeros`.
246250
"""
247251
function tzeros(sys::TransferFunction)
248252
if issiso(sys)
@@ -260,7 +264,11 @@ function tzeros(A::AbstractMatrix, B::AbstractMatrix, C::AbstractMatrix, D::Abst
260264
tzeros(A2, B2, C2, D2)
261265
end
262266

263-
function tzeros(A::AbstractMatrix{T}, B::AbstractMatrix{T}, C::AbstractMatrix{T}, D::AbstractMatrix{T}; extra::Val{E} = Val{false}(), kwargs...) where {T <: Union{AbstractFloat,Complex{<:AbstractFloat}}, E}
267+
function tzeros(A::AbstractMatrix{T}, B::AbstractMatrix{T}, C::AbstractMatrix{T}, D::AbstractMatrix{T}; extra::Val{E} = Val{false}(), balance=true, kwargs...) where {T <: BlasFloat, E}
268+
if balance
269+
A, B, C = balance_statespace(A, B, C)
270+
end
271+
264272
(z, iz, KRInfo) = MatrixPencils.spzeros(A, I, B, C, D; kwargs...)
265273
if E
266274
return (z, iz, KRInfo)
@@ -269,6 +277,111 @@ function tzeros(A::AbstractMatrix{T}, B::AbstractMatrix{T}, C::AbstractMatrix{T}
269277
end
270278
end
271279

280+
function tzeros(A::AbstractMatrix{T}, B::AbstractMatrix{T}, C::AbstractMatrix{T}, D::AbstractMatrix{T}; extra::Val{E} = Val{false}(), kwargs...) where {T <: Union{AbstractFloat,Complex{<:AbstractFloat}}, E}
281+
isempty(A) && return complex(T)[]
282+
283+
# Balance the system
284+
A, B, C = balance_statespace(A, B, C; verbose=false)
285+
286+
# Compute a good tolerance
287+
meps = 10*eps(real(T))*norm([A B; C D])
288+
289+
# Step 1:
290+
A_r, B_r, C_r, D_r = reduce_sys(A, B, C, D, meps)
291+
292+
# Step 2: (conjugate transpose should be avoided since single complex zeros get conjugated)
293+
A_rc, B_rc, C_rc, D_rc = reduce_sys(copy(transpose(A_r)), copy(transpose(C_r)), copy(transpose(B_r)), copy(transpose(D_r)), meps)
294+
295+
# Step 3:
296+
# Compress cols of [C D] to [0 Df]
297+
mat = [C_rc D_rc]
298+
Wr = qr(mat').Q * I
299+
W = reverse(Wr, dims=2)
300+
mat = mat*W
301+
if fastrank(mat', meps) > 0
302+
nf = size(A_rc, 1)
303+
m = size(D_rc, 2)
304+
Af = ([A_rc B_rc] * W)[1:nf, 1:nf]
305+
Bf = ([Matrix{T}(I, nf, nf) zeros(nf, m)] * W)[1:nf, 1:nf]
306+
Z = schur(complex.(Af), complex.(Bf)) # Schur instead of eigvals to handle BigFloat
307+
zs = Z.values
308+
ControlSystemsBase._fix_conjugate_pairs!(zs) # Generalized eigvals does not return exact conj. pairs
309+
else
310+
zs = complex(T)[]
311+
end
312+
return zs
313+
end
314+
315+
"""
316+
reduce_sys(A::AbstractMatrix, B::AbstractMatrix, C::AbstractMatrix, D::AbstractMatrix, meps::AbstractFloat)
317+
Implements REDUCE in the Emami-Naeini & Van Dooren paper. Returns transformed
318+
A, B, C, D matrices. These are empty if there are no zeros.
319+
"""
320+
function reduce_sys(A::AbstractMatrix, B::AbstractMatrix, C::AbstractMatrix, D::AbstractMatrix, meps::AbstractFloat)
321+
T = promote_type(eltype(A), eltype(B), eltype(C), eltype(D))
322+
Cbar, Dbar = C, D
323+
if isempty(A)
324+
return A, B, C, D
325+
end
326+
while true
327+
# Compress rows of D
328+
U = qr(D).Q
329+
D = U'D
330+
C = U'C
331+
sigma = fastrank(D, meps)
332+
Cbar = C[1:sigma, :]
333+
Dbar = D[1:sigma, :]
334+
Ctilde = C[(1 + sigma):end, :]
335+
if sigma == size(D, 1)
336+
break
337+
end
338+
339+
# Compress columns of Ctilde
340+
V = reverse(qr(Ctilde').Q * I, dims=2)
341+
Sj = Ctilde*V
342+
rho = fastrank(Sj', meps)
343+
nu = size(Sj, 2) - rho
344+
345+
if rho == 0
346+
break
347+
elseif nu == 0
348+
# System has no zeros, return empty matrices
349+
A = B = Cbar = Dbar = Matrix{T}(undef, 0,0)
350+
break
351+
end
352+
# Update System
353+
n, m = size(B)
354+
Vm = [V zeros(T, n, m); zeros(T, m, n) Matrix{T}(I, m, m)] # I(m) is not used for type stability reasons (as of julia v1.7)
355+
if sigma > 0
356+
M = [A B; Cbar Dbar]
357+
Vs = [copy(V') zeros(T, n, sigma) ; zeros(T, sigma, n) Matrix{T}(I, sigma, sigma)]
358+
else
359+
M = [A B]
360+
Vs = copy(V')
361+
end
362+
sigma, rho, nu
363+
M = Vs * M * Vm
364+
A = M[1:nu, 1:nu]
365+
B = M[1:nu, (nu + rho + 1):end]
366+
C = M[(nu + 1):end, 1:nu]
367+
D = M[(nu + 1):end, (nu + rho + 1):end]
368+
end
369+
return A, B, Cbar, Dbar
370+
end
371+
372+
# Determine the number of non-zero rows, with meps as a tolerance. For an
373+
# upper-triangular matrix, this is a good proxy for determining the row-rank.
374+
function fastrank(A::AbstractMatrix, meps::Real)
375+
n, m = size(A)
376+
if n*m == 0 return 0 end
377+
norms = Vector{real(eltype(A))}(undef, n)
378+
for i = 1:n
379+
norms[i] = norm(A[i, :])
380+
end
381+
mrank = sum(norms .> meps)
382+
return mrank
383+
end
384+
272385
"""
273386
relative_gain_array(G, w::AbstractVector)
274387
relative_gain_array(G, w::Number)

lib/ControlSystemsBase/src/types/conversion.jl

+4-4
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@ The inverse of `sysb, T = balance_statespace(sys)` is given by `similarity_trans
159159
160160
This is not the same as finding a balanced realization with equal and diagonal observability and reachability gramians, see [`balreal`](@ref)
161161
"""
162-
function balance_statespace(A::AbstractMatrix, B::AbstractMatrix, C::AbstractMatrix, perm::Bool=false)
162+
function balance_statespace(A::AbstractMatrix, B::AbstractMatrix, C::AbstractMatrix, perm::Bool=false; verbose=true)
163163
try
164164
return _balance_statespace(A,B,C, perm)
165165
catch
166-
@warn "Unable to balance state-space, returning original system" maxlog=10
166+
verbose && @warn "Unable to balance state-space, returning original system" maxlog=10
167167
return A,B,C,convert(typeof(A), I(size(A, 1)))
168168
end
169169
end
@@ -175,8 +175,8 @@ end
175175
# balance_statespace(A2, B2, C2, perm)
176176
# end
177177

178-
function balance_statespace(sys::S, perm::Bool=false) where S <: AbstractStateSpace
179-
A, B, C, T = balance_statespace(sys.A,sys.B,sys.C, perm)
178+
function balance_statespace(sys::S, perm::Bool=false; kwargs...) where S <: AbstractStateSpace
179+
A, B, C, T = balance_statespace(sys.A,sys.B,sys.C, perm; kwargs...)
180180
ss(A,B,C,sys.D,sys.timeevol), T
181181
end
182182

lib/ControlSystemsBase/test/test_analysis.jl

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
@test_throws MethodError poles(big(1.0)*ssrand(1,1,1)) # This errors before loading GenericLinearAlgebra
2-
using GenericLinearAlgebra # Required to compute eigvals of a matrix with exotic element types
1+
@test_throws MethodError poles(big(1.0)*ssrand(1,1,1)) # This errors before loading GenericSchur
2+
using GenericSchur # Required to compute eigvals (in tzeros and poles) of a matrix with exotic element types
33
@testset "test_analysis" begin
44
es(x) = sort(x, by=LinearAlgebra.eigsortby)
55
## tzeros ##
@@ -58,6 +58,7 @@ C = [0 -1 0]
5858
D = [0]
5959
ex_6 = ss(A, B, C, D)
6060
@test tzeros(ex_6) == [2] # From paper: "Our algorithm will extract the singular part of S(A) and will yield a regular pencil containing the single zero at 2."
61+
@test_broken tzeros(big(1.0)ex_6) == [2]
6162
@test ControlSystemsBase.count_integrators(ex_6) == 2
6263

6364
@test ss(A, [0 0 1]', C, D) == ex_6
@@ -79,6 +80,7 @@ D = [0]
7980
ex_8 = ss(A, B, C, D)
8081
# TODO : there may be a way to improve the precision of this example.
8182
@test tzeros(ex_8) [-1.0, -1.0] atol=1e-7
83+
@test tzeros(big(1)ex_8) [-1.0, -1.0] atol=1e-7
8284
@test ControlSystemsBase.count_integrators(ex_8) == 0
8385

8486
# Example 9
@@ -102,6 +104,7 @@ D = [0 0;
102104
0 0]
103105
ex_11 = ss(A, B, C, D)
104106
@test tzeros(ex_11) [4.0, -3.0]
107+
@test tzeros(big(1)ex_11) [4.0, -3.0]
105108

106109
# Test for multiple zeros, siso tf
107110
s = tf("s")
@@ -368,4 +371,16 @@ P = let
368371
end
369372
@test ControlSystemsBase.count_integrators(P) == 2
370373

374+
## Difficult test case for zeros
375+
G = let
376+
tempA = [-0.6841991610512457 -0.0840213470263692 -0.0004269818661494616 -2.7625001165862086e-18; 2.081491248616774 0.0 0.0 8.404160870560225e-18; 0.0 24.837541148074962 0.12622006230897712 0.0; -1.2211265763794115e-14 -2.778983834717109e8 -1.4122312296634873e6 -4.930380657631326e-32]
377+
tempB = [-0.5316255605902501; 2.0811471051085637; -45.068824982602656; 5.042589978197361e8;;]
378+
tempC = [0.0 0.0 0.954929658551372 0.0]
379+
tempD = [0.0;;]
380+
ss(tempA, tempB, tempC, tempD)
381+
end
382+
383+
@test length(tzeros(G)) == 3
384+
@test es(tzeros(G)) es(tzeros(big(1)G))
385+
371386
end

0 commit comments

Comments
 (0)