Skip to content

Commit bbbbaff

Browse files
authored
add \ for ss and handle some cases of multiplication with improper tf (#930)
* add \ for ss and handle some cases of multiplication with improper tf * bump version * fix stray end * ch error type
1 parent 96cf030 commit bbbbaff

File tree

6 files changed

+114
-4
lines changed

6 files changed

+114
-4
lines changed

lib/ControlSystemsBase/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name = "ControlSystemsBase"
22
uuid = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
33
authors = ["Dept. Automatic Control, Lund University"]
44
repo = "https://github.com/JuliaControl/ControlSystems.jl.git"
5-
version = "1.10.2"
5+
version = "1.10.3"
66

77
[deps]
88
DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2"

lib/ControlSystemsBase/src/types/Lti.jl

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
11
abstract type LTISystem{TE<:TimeEvolution} <: AbstractSystem end
22
+(sys1::LTISystem, sys2::LTISystem) = +(promote(sys1, sys2)...)
33
-(sys1::LTISystem, sys2::LTISystem) = -(promote(sys1, sys2)...)
4-
*(sys1::LTISystem, sys2::LTISystem) = *(promote(sys1, sys2)...)
4+
function *(sys1::LTISystem, sys2::LTISystem)
5+
zeroexcess(sys) = length(numvec(sys)[]) - length(denvec(sys)[])
6+
try
7+
*(promote(sys1, sys2)...)
8+
catch e
9+
e isa ImproperException || rethrow()
10+
if sys1 isa AbstractStateSpace && sys2 isa TransferFunction
11+
issiso(sys2) || rethrow() # Can't invert MIMO tf
12+
zeroexcess(sys2) <= sys1.nx || rethrow() # Quotient can't be proper
13+
Q = sys1 / ss(inv(sys2))
14+
elseif sys1 isa TransferFunction && sys2 isa AbstractStateSpace
15+
issiso(sys1) || rethrow() # Can't invert MIMO tf
16+
zeroexcess(sys1) <= sys2.nx || rethrow() # Quotient can't be proper
17+
Q = ss(inv(sys1)) \ sys2
18+
else
19+
rethrow()
20+
end
21+
Q = balance_statespace(Q)[1]
22+
if max(maximum(abs, Q.A), maximum(abs, Q.B), maximum(abs, Q.C), maximum(abs, Q.D)) > 1e7
23+
@warn "Possible numerical instability detected: Multiplication of a statespace system and a non-proper transfer function may result in numerical inaccuracy. Verify result carefully, and consider making use of DescriptorSystems.jl to represent this product as a DescriptorSystem with non-unit descriptor matrix if result is inaccurate."
24+
end
25+
return Q
26+
end
27+
end
528
/(sys1::LTISystem, sys2::LTISystem) = /(promote(sys1, sys2)...)
629

730
# Fallback number

lib/ControlSystemsBase/src/types/StateSpace.jl

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,47 @@ function Base.:(/)(sys1::AbstractStateSpace{TE}, sys2::AbstractStateSpace{TE};
428428
return StateSpace{TE, T}(Ai, Bi, Ci, Di, timeevol)
429429
end
430430

431+
function Base.:(\)(sys1::AbstractStateSpace{TE}, sys2::AbstractStateSpace{TE};
432+
atol::Real = 0, atol1::Real = atol, atol2::Real = atol,
433+
rtol::Real = max(size(sys1.A,1),size(sys2.A,1))*eps(real(float(one(numeric_type(sys1)))))*iszero(min(atol1,atol2))) where {TE<:ControlSystemsBase.TimeEvolution}
434+
T1 = float(numeric_type(sys1))
435+
T2 = float(numeric_type(sys2))
436+
T = promote_type(T1,T2)
437+
timeevol = common_timeevol(sys1, sys2)
438+
ny2, nu2 = sys2.ny, sys2.nu
439+
nu2 == ny2 || error("The system sys2 must be square")
440+
ny1, nu1 = sys1.ny, sys1.nu
441+
nu1 == nu2 || error("The systems sys1 and sys2 must have the same number of inputs")
442+
nx1 = sys1.nx
443+
nx2 = sys2.nx
444+
if nx2 > 0
445+
A, B, C, D = ssdata([sys1 sys2])
446+
Ai = [A B[:,1:nu1]; C D[:,1:nu1]]
447+
Ei = [I zeros(T,nx1+nx2,nu1); zeros(T,nu1,nx1+nx2+nu1)] |> Matrix # TODO: rm call to Matrix when type piracy in https://github.com/JuliaLinearAlgebra/LinearMaps.jl/issues/219 is fixed
448+
MatrixPencils.isregular(Ai, Ei; atol1, atol2, rtol) ||
449+
error("The system sys1 is not invertible")
450+
Bi = [B[:,nu1+1:nu1+nu2]; D[:,nu1+1:nu1+nu2]]
451+
Ci = [zeros(T,ny1,nx1+nx2) -I]
452+
Di = zeros(T,ny1,nu2)
453+
Ai, Ei, Bi, Ci, Di = MatrixPencils.lsminreal(Ai, Ei, Bi, Ci, Di; fast = true, atol1 = 0, atol2, rtol, contr = true, obs = true, noseig = true)
454+
if Ei != I
455+
luE = lu!(Ei, check=false)
456+
issuccess(luE) || throw(ArgumentError("The system sys1 is not invertible"))
457+
Ai = luE\Ai
458+
Bi = luE\Bi
459+
end
460+
else
461+
D1 = T.(sys1.D)
462+
LUD = lu(D1)
463+
(norm(D1,Inf) <= atol1 || rcond(LUD.U) <= 10*nu1*eps(real(float(one(T))))) &&
464+
error("The system sys1 is not invertible")
465+
Ai, Bi, Ci, Di = ssdata(sys2)
466+
ldiv!(LUD, Ci); ldiv!(LUD, Di)
467+
end
468+
469+
return StateSpace{TE, T}(Ai, Bi, Ci, Di, timeevol)
470+
end
471+
431472
function /(n::Number, sys::ST) where ST <: AbstractStateSpace
432473
# Ensure s.D is invertible
433474
A, B, C, D = ssdata(sys)
@@ -445,6 +486,22 @@ Base.inv(sys::AbstractStateSpace) = 1/sys
445486
Base.:\(n::Number, sys::ST) where ST <: AbstractStateSpace = basetype(ST)(sys.A, sys.B, sys.C/n, sys.D/n, sys.timeevol)
446487

447488

489+
function Base.adjoint(sys::ST) where ST <: AbstractStateSpace{Continuous}
490+
return basetype(ST)(-sys.A', -sys.C', sys.B', sys.D')
491+
end
492+
493+
function Base.adjoint(sys::ST) where ST <: AbstractStateSpace{<:Discrete}
494+
nx, ny, nu = sys.nx, sys.ny, sys.nu
495+
T = numeric_type(sys)
496+
return basetype(ST)(
497+
[sys.A' sys.C'; zeros(T,ny,nx+ny)],
498+
[zeros(T,nx,ny) ; -I],
499+
[sys.B' zeros(T,nu,ny)], copy(sys.D'),
500+
sys.timeevol
501+
)
502+
end
503+
504+
448505
#####################################################################
449506
## Indexing Functions ##
450507
#####################################################################

lib/ControlSystemsBase/src/types/conversion.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,14 @@ function Base.convert(::Type{StateSpace}, G::TransferFunction{TE,<:SisoTf{T0}};
8686
convert(StateSpace{TE,T}, G; kwargs...)
8787
end
8888

89+
struct ImproperException <: Exception end
90+
91+
Base.showerror(io::IO, e::ImproperException) = print(io, "System is improper, a state-space representation is impossible")
92+
8993
# Note: balancing is only applied by default for floating point types, integer systems are not balanced since that would change the type.
9094
function Base.convert(::Type{StateSpace{TE,T}}, G::TransferFunction; balance=!(T <: Union{Integer, Rational, ForwardDiff.Dual})) where {TE,T<:Number}
9195
if !isproper(G)
92-
error("System is improper, a state-space representation is impossible")
96+
throw(ImproperException())
9397
end
9498

9599
ny, nu = size(G)

lib/ControlSystemsBase/test/test_conversion.jl

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ Hd = zpk([], [1, 0.5], 1.0, h)
177177

178178

179179
# Error, not proper
180-
@test_throws ErrorException ss(tf([1,0],[1]))
180+
@test_throws ControlSystemsBase.ImproperException ss(tf([1,0],[1]))
181181

182182
s1 = HeteroStateSpace(zeros(Int, 1,1),zeros(Int, 1,1),zeros(Int, 1,1),zeros(Int, 1,1))
183183
s2 = HeteroStateSpace(zeros(Float64, 1,1),zeros(Float64, 1,1),zeros(Float64, 1,1),zeros(Float64, 1,1))
@@ -201,4 +201,28 @@ sys.matrix[1,1] = 0
201201
syszpk = zpk(sys)
202202
@test syszpk isa TransferFunction{Continuous, ControlSystemsBase.SisoZpk{Float64, ComplexF64}}
203203

204+
## Test multiplication of improper transfer function and statespace system
205+
P = DemoSystems.double_mass_model()
206+
C = tf('s')
207+
@test norm(P*C - C*P) < 1e-10
208+
mp = minreal(minreal(tf(P)*C) - P*C)
209+
@test hinfnorm(mp)[1] < 1e-10
210+
@inferred P*C
211+
212+
@test_logs (:warn,"Possible numerical instability detected: Multiplication of a statespace system and a non-proper transfer function may result in numerical inaccuracy. Verify result carefully, and consider making use of DescriptorSystems.jl to represent this product as a DescriptorSystem with non-unit descriptor matrix if result is inaccurate.") P*tf('s')^3
213+
214+
@test_throws ControlSystemsBase.ImproperException P*tf('s')^5
215+
# Discrete
216+
217+
P = c2d(P, 0.01, :fwdeuler) # to get poles exactly at 1
218+
C = tf('z', 0.01)-1
219+
@test norm(minreal(P*C - C*P)) < 1e-10
220+
mp = minreal(minreal(tf(P)*C) - P*C)
221+
@test hinfnorm(mp)[1] < 1e-10
222+
@inferred P*C
223+
224+
# @test_logs (:warn,"Possible numerical instability detected: Multiplication of a statespace system and a non-proper transfer function may result in numerical inaccuracy. Verify result carefully, and consider making use of DescriptorSystems.jl to represent this product as a DescriptorSystem with non-unit descriptor matrix if result is inaccurate.") P*C^3
225+
226+
@test_throws ControlSystemsBase.ImproperException P*C^5
227+
204228
end

lib/ControlSystemsBase/test/test_statespace.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@
175175
G2s = ss(G2)
176176
@test tf(G2s / G1s) G2 / G1
177177

178+
@test tf(G1s \ G2s) G2 / G1
179+
178180
fsys = ss(1,1,1,0)/3 # Int becomes FLoat after division
179181
@test fsys.B[]*fsys.C[] == 1/3
180182

0 commit comments

Comments
 (0)