Skip to content

Commit

Permalink
add \ for ss and handle some cases of multiplication with improper …
Browse files Browse the repository at this point in the history
…tf (#930)

* add \ for ss and handle some cases of multiplication with improper tf

* bump version

* fix stray end

* ch error type
  • Loading branch information
baggepinnen authored Aug 2, 2024
1 parent 96cf030 commit bbbbaff
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 4 deletions.
2 changes: 1 addition & 1 deletion lib/ControlSystemsBase/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name = "ControlSystemsBase"
uuid = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
authors = ["Dept. Automatic Control, Lund University"]
repo = "https://github.com/JuliaControl/ControlSystems.jl.git"
version = "1.10.2"
version = "1.10.3"

[deps]
DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2"
Expand Down
25 changes: 24 additions & 1 deletion lib/ControlSystemsBase/src/types/Lti.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
abstract type LTISystem{TE<:TimeEvolution} <: AbstractSystem end
+(sys1::LTISystem, sys2::LTISystem) = +(promote(sys1, sys2)...)
-(sys1::LTISystem, sys2::LTISystem) = -(promote(sys1, sys2)...)
*(sys1::LTISystem, sys2::LTISystem) = *(promote(sys1, sys2)...)
function *(sys1::LTISystem, sys2::LTISystem)
zeroexcess(sys) = length(numvec(sys)[]) - length(denvec(sys)[])
try
*(promote(sys1, sys2)...)
catch e
e isa ImproperException || rethrow()
if sys1 isa AbstractStateSpace && sys2 isa TransferFunction
issiso(sys2) || rethrow() # Can't invert MIMO tf
zeroexcess(sys2) <= sys1.nx || rethrow() # Quotient can't be proper
Q = sys1 / ss(inv(sys2))
elseif sys1 isa TransferFunction && sys2 isa AbstractStateSpace
issiso(sys1) || rethrow() # Can't invert MIMO tf
zeroexcess(sys1) <= sys2.nx || rethrow() # Quotient can't be proper
Q = ss(inv(sys1)) \ sys2
else
rethrow()
end
Q = balance_statespace(Q)[1]
if max(maximum(abs, Q.A), maximum(abs, Q.B), maximum(abs, Q.C), maximum(abs, Q.D)) > 1e7
@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."
end
return Q
end
end
/(sys1::LTISystem, sys2::LTISystem) = /(promote(sys1, sys2)...)

# Fallback number
Expand Down
57 changes: 57 additions & 0 deletions lib/ControlSystemsBase/src/types/StateSpace.jl
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,47 @@ function Base.:(/)(sys1::AbstractStateSpace{TE}, sys2::AbstractStateSpace{TE};
return StateSpace{TE, T}(Ai, Bi, Ci, Di, timeevol)
end

function Base.:(\)(sys1::AbstractStateSpace{TE}, sys2::AbstractStateSpace{TE};
atol::Real = 0, atol1::Real = atol, atol2::Real = atol,
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}
T1 = float(numeric_type(sys1))
T2 = float(numeric_type(sys2))
T = promote_type(T1,T2)
timeevol = common_timeevol(sys1, sys2)
ny2, nu2 = sys2.ny, sys2.nu
nu2 == ny2 || error("The system sys2 must be square")
ny1, nu1 = sys1.ny, sys1.nu
nu1 == nu2 || error("The systems sys1 and sys2 must have the same number of inputs")
nx1 = sys1.nx
nx2 = sys2.nx
if nx2 > 0
A, B, C, D = ssdata([sys1 sys2])
Ai = [A B[:,1:nu1]; C D[:,1:nu1]]
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
MatrixPencils.isregular(Ai, Ei; atol1, atol2, rtol) ||
error("The system sys1 is not invertible")
Bi = [B[:,nu1+1:nu1+nu2]; D[:,nu1+1:nu1+nu2]]
Ci = [zeros(T,ny1,nx1+nx2) -I]
Di = zeros(T,ny1,nu2)
Ai, Ei, Bi, Ci, Di = MatrixPencils.lsminreal(Ai, Ei, Bi, Ci, Di; fast = true, atol1 = 0, atol2, rtol, contr = true, obs = true, noseig = true)
if Ei != I
luE = lu!(Ei, check=false)
issuccess(luE) || throw(ArgumentError("The system sys1 is not invertible"))
Ai = luE\Ai
Bi = luE\Bi
end
else
D1 = T.(sys1.D)
LUD = lu(D1)
(norm(D1,Inf) <= atol1 || rcond(LUD.U) <= 10*nu1*eps(real(float(one(T))))) &&
error("The system sys1 is not invertible")
Ai, Bi, Ci, Di = ssdata(sys2)
ldiv!(LUD, Ci); ldiv!(LUD, Di)
end

return StateSpace{TE, T}(Ai, Bi, Ci, Di, timeevol)
end

function /(n::Number, sys::ST) where ST <: AbstractStateSpace
# Ensure s.D is invertible
A, B, C, D = ssdata(sys)
Expand All @@ -445,6 +486,22 @@ Base.inv(sys::AbstractStateSpace) = 1/sys
Base.:\(n::Number, sys::ST) where ST <: AbstractStateSpace = basetype(ST)(sys.A, sys.B, sys.C/n, sys.D/n, sys.timeevol)


function Base.adjoint(sys::ST) where ST <: AbstractStateSpace{Continuous}
return basetype(ST)(-sys.A', -sys.C', sys.B', sys.D')
end

function Base.adjoint(sys::ST) where ST <: AbstractStateSpace{<:Discrete}
nx, ny, nu = sys.nx, sys.ny, sys.nu
T = numeric_type(sys)
return basetype(ST)(
[sys.A' sys.C'; zeros(T,ny,nx+ny)],
[zeros(T,nx,ny) ; -I],
[sys.B' zeros(T,nu,ny)], copy(sys.D'),
sys.timeevol
)
end


#####################################################################
## Indexing Functions ##
#####################################################################
Expand Down
6 changes: 5 additions & 1 deletion lib/ControlSystemsBase/src/types/conversion.jl
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,14 @@ function Base.convert(::Type{StateSpace}, G::TransferFunction{TE,<:SisoTf{T0}};
convert(StateSpace{TE,T}, G; kwargs...)
end

struct ImproperException <: Exception end

Base.showerror(io::IO, e::ImproperException) = print(io, "System is improper, a state-space representation is impossible")

# Note: balancing is only applied by default for floating point types, integer systems are not balanced since that would change the type.
function Base.convert(::Type{StateSpace{TE,T}}, G::TransferFunction; balance=!(T <: Union{Integer, Rational, ForwardDiff.Dual})) where {TE,T<:Number}
if !isproper(G)
error("System is improper, a state-space representation is impossible")
throw(ImproperException())
end

ny, nu = size(G)
Expand Down
26 changes: 25 additions & 1 deletion lib/ControlSystemsBase/test/test_conversion.jl
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ Hd = zpk([], [1, 0.5], 1.0, h)


# Error, not proper
@test_throws ErrorException ss(tf([1,0],[1]))
@test_throws ControlSystemsBase.ImproperException ss(tf([1,0],[1]))

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

## Test multiplication of improper transfer function and statespace system
P = DemoSystems.double_mass_model()
C = tf('s')
@test norm(P*C - C*P) < 1e-10
mp = minreal(minreal(tf(P)*C) - P*C)
@test hinfnorm(mp)[1] < 1e-10
@inferred P*C

@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

@test_throws ControlSystemsBase.ImproperException P*tf('s')^5
# Discrete

P = c2d(P, 0.01, :fwdeuler) # to get poles exactly at 1
C = tf('z', 0.01)-1
@test norm(minreal(P*C - C*P)) < 1e-10
mp = minreal(minreal(tf(P)*C) - P*C)
@test hinfnorm(mp)[1] < 1e-10
@inferred P*C

# @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

@test_throws ControlSystemsBase.ImproperException P*C^5

end
2 changes: 2 additions & 0 deletions lib/ControlSystemsBase/test/test_statespace.jl
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@
G2s = ss(G2)
@test tf(G2s / G1s) G2 / G1

@test tf(G1s \ G2s) G2 / G1

fsys = ss(1,1,1,0)/3 # Int becomes FLoat after division
@test fsys.B[]*fsys.C[] == 1/3

Expand Down

4 comments on commit bbbbaff

@baggepinnen
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register subdir=lib/ControlSystemsBase

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/112285

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a ControlSystemsBase-v1.10.3 -m "<description of version>" bbbbaffe82e13e15cedd11e4bd9ac9cdcfd4b89b
git push origin ControlSystemsBase-v1.10.3

@baggepinnen
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register subdir=lib/ControlSystemsBase

Release notes:

  • Added \ between statespace systems.
  • Handle multiplication of statespace system and non-proper transfer function if the product is proper.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request updated: JuliaRegistries/General/112285

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a ControlSystemsBase-v1.10.3 -m "<description of version>" bbbbaffe82e13e15cedd11e4bd9ac9cdcfd4b89b
git push origin ControlSystemsBase-v1.10.3

Please sign in to comment.