Skip to content

Commit

Permalink
Merge pull request #971 from JuliaControl/tf2ss
Browse files Browse the repository at this point in the history
improve tf2ss conversion
  • Loading branch information
baggepinnen authored Feb 14, 2025
2 parents c66b3d1 + aba649d commit c1cf26a
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 87 deletions.
5 changes: 4 additions & 1 deletion docs/src/man/creating_systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ in discrete time, is created using
```julia
ss(A,B,C,D) # Continuous-time system
ss(A,B,C,D,Ts) # Discrete-time system
ss(P; balance=true, minimal=false) # Convert transfer function P to state space
```
and they behave similarly to transfer functions.

Expand All @@ -118,7 +119,7 @@ HeteroStateSpace(sys, to_static)
```
Notice the different matrix types used.

To associate **names** with states, inputs and outputs, see [`named_ss`](https://juliacontrol.github.io/RobustAndOptimalControl.jl/dev/#Named-systems) from RobustAndOptimalControl.jl.
To associate **names** with state variables, inputs and outputs, see [`named_ss`](https://juliacontrol.github.io/RobustAndOptimalControl.jl/dev/#Named-systems) from RobustAndOptimalControl.jl.


## Converting between types
Expand All @@ -137,6 +138,8 @@ Sample Time: 0.1 (seconds)
Discrete-time transfer function model
```

When a transfer function `P` is converted to a state-space model using `ss(P; balance=true, minimal=false)`, the user may choose whether to balance the state-space model (default=true) and/or to return a minimal realization (default=false).


## Converting between continuous and discrete time
A continuous-time system represents differential equations or a transfer function in the [Laplace domain](https://en.wikipedia.org/wiki/Laplace_transform), while a discrete-time system represents difference equations or a transfer function in the [Z-domain](https://en.wikipedia.org/wiki/Z-transform).
Expand Down
6 changes: 3 additions & 3 deletions lib/ControlSystemsBase/src/timeresp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ end

Base.step(sys::LTISystem, tfinal::Real; kwargs...) = step(sys, _default_time_vector(sys, tfinal); kwargs...)
Base.step(sys::LTISystem; kwargs...) = step(sys, _default_time_vector(sys); kwargs...)
Base.step(sys::TransferFunction, t::AbstractVector; kwargs...) = step(ss(sys), t::AbstractVector; kwargs...)
Base.step(sys::TransferFunction, t::AbstractVector; kwargs...) = step(ss(sys, minimal=numeric_type(sys) isa BlasFloat), t::AbstractVector; kwargs...)

"""
y, t, x = impulse(sys[, tfinal])
Expand Down Expand Up @@ -119,7 +119,7 @@ end

impulse(sys::LTISystem, tfinal::Real; kwargs...) = impulse(sys, _default_time_vector(sys, tfinal); kwargs...)
impulse(sys::LTISystem; kwargs...) = impulse(sys, _default_time_vector(sys); kwargs...)
impulse(sys::TransferFunction, t::AbstractVector; kwargs...) = impulse(ss(sys), t; kwargs...)
impulse(sys::TransferFunction, t::AbstractVector; kwargs...) = impulse(ss(sys, minimal=numeric_type(sys) isa BlasFloat), t; kwargs...)

"""
result = lsim(sys, u[, t]; x0, method])
Expand Down Expand Up @@ -301,7 +301,7 @@ function lsim(sys::AbstractStateSpace, u::Function, t::AbstractVector;
end


lsim(sys::TransferFunction, args...; kwargs...) = lsim(ss(sys), args...; kwargs...)
lsim(sys::TransferFunction, args...; kwargs...) = lsim(ss(sys, minimal=numeric_type(sys) isa BlasFloat), args...; kwargs...)


"""
Expand Down
5 changes: 4 additions & 1 deletion lib/ControlSystemsBase/src/types/StateSpace.jl
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ StateSpace(sys::LTISystem; kwargs...) = convert(StateSpace, sys; kwargs...)
"""
sys = ss(A, B, C, D) # Continuous
sys = ss(A, B, C, D, Ts) # Discrete
sys = ss(P::TransferFunction; balance=true, minimal=false) # Convert TransferFunction to StateSpace
Create a state-space model `sys::StateSpace{TE, T}`
with matrix element type `T` and TE is `Continuous` or `<:Discrete`.
Expand All @@ -138,7 +139,9 @@ Otherwise, this is a discrete-time model with sampling period `Ts`.
`D` may be specified as `0` in which case a zero matrix of appropriate size is constructed automatically.
`sys = ss(D [, Ts])` specifies a static gain matrix `D`.
To associate names with states, inputs and outputs, see [`named_ss`](https://juliacontrol.github.io/RobustAndOptimalControl.jl/dev/#Named-systems).
When a transfer function `P` is converted to a state-space model, the user may choose whether to balance the state-space model (default=true) and/or to return a minimal realization (default=false). This conversion has more keyword argument options, see `?ControlSystemsBase.MatrixPencils.rm2ls` for additional details.
To associate names with state variables, inputs and outputs, see [`named_ss`](https://juliacontrol.github.io/RobustAndOptimalControl.jl/dev/#Named-systems).
"""
ss(args...;kwargs...) = StateSpace(args...;kwargs...)

Expand Down
133 changes: 58 additions & 75 deletions lib/ControlSystemsBase/src/types/conversion.jl
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
# Base.convert(::Type{<:SisoTf}, b::Real) = Base.convert(SisoRational, b)
# Base.convert{T<:Real}(::Type{<:SisoZpk}, b::T) = SisoZpk(T[], T[], b)
# Base.convert{T<:Real}(::Type{<:SisoRational}, b::T) = SisoRational([b], [one(T)])
# Base.convert{T1}(::Type{SisoRational{Vector{T1}}}, t::SisoRational) = SisoRational(Polynomial(T1.(t.num.coeffs$1),Polynomial(T1.(t.den.coeffs$1))
# Base.convert(::Type{<:StateSpace}, t::Real) = ss(t)
#

#
# function Base.convert{T<:AbstractMatrix{<:Number}}(::Type{StateSpace{T}}, s::StateSpace)
# AT = promote_type(T, arraytype(s))
# StateSpace{AT}(AT(s.A),AT(s.B),AT(s.C),AT(s.D), s.timeevol, s.statenames, s.inputnames, s.outputnames)
# end

# TODO Fix these to use proper constructors
# NOTE: no real need to convert numbers to transfer functions, have addition methods..
# How to convert a number to either Continuous or Discrete transfer function
Expand Down Expand Up @@ -42,19 +29,7 @@ Base.convert(::Type{TransferFunction{TE,SisoZpk{T,TR}}}, d::Number) where {TE, T

Base.convert(::Type{StateSpace{TE,T}}, d::Number) where {TE, T} =
convert(StateSpace{TE,T}, fill(d, (1,1)))
#
# Base.convert(::Type{TransferFunction{Continuous,<:SisoRational{T}}}, b::Number) where {T} = tf(T(b), Continuous())
# Base.convert(::Type{TransferFunction{Continuous,<:SisoZpk{T, TR}}}, b::Number) where {T, TR} = zpk(T(b), Continuous())
# Base.convert(::Type{StateSpace{Continuous,T, MT}}, b::Number) where {T, MT} = convert(StateSpace{Continuous,T, MT}, fill(b, (1,1)))

# function Base.convert{T<:Real,S<:TransferFunction}(::Type{S}, b::VecOrMat{T})
# r = Matrix{S}(size(b,2),1)
# for j=1:size(b,2)
# r[j] = vcat(map(k->convert(S,k),b[:,j])...)
# end
# hcat(r...)
# end
#


function convert(::Type{TransferFunction{TE,S}}, G::TransferFunction) where {TE,S}
Gnew_matrix = convert.(S, G.matrix)
Expand Down Expand Up @@ -92,77 +67,85 @@ 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)
throw(ImproperException())
end
function Base.convert(::Type{StateSpace{TE,T}}, G::TransferFunction; balance=!(T <: Union{Integer, Rational, ForwardDiff.Dual}), minimal=false, contr=minimal, obs=minimal, noseig=minimal, kwargs...) where {TE,T<:Number}

ny, nu = size(G)

# A, B, C, D matrices for each element of the transfer function matrix
abcd_vec = [siso_tf_to_ss(T, g) for g in G.matrix[:]]
NUM = numpoly(G)
DEN = denpoly(G)
(A, E, B, C, D, blkdims) = MatrixPencils.rm2ls(NUM, DEN; contr, obs, noseig, minimal, kwargs...)
E == I || throw(ImproperException())

# Number of states for each transfer function element realization
nvec = [size(abcd[1], 1) for abcd in abcd_vec]
ntot = sum(nvec)

A = zeros(T, (ntot, ntot))
B = zeros(T, (ntot, nu))
C = zeros(T, (ny, ntot))
D = zeros(T, (ny, nu))
# if !isproper(G)
# throw(ImproperException())
# end

inds = -1:0
for j=1:nu
for i=1:ny
k = (j-1)*ny + i
# ny, nu = size(G)

# states corresponding to the transfer function element (i,j)
inds = (inds.stop+1):(inds.stop+nvec[k])
# # A, B, C, D matrices for each element of the transfer function matrix
# abcd_vec = [siso_tf_to_ss(T, g) for g in G.matrix[:]]

A[inds,inds], B[inds,j:j], C[i:i,inds], D[i:i,j:j] = abcd_vec[k]
end
end
# # Number of states for each transfer function element realization
# nvec = [size(abcd[1], 1) for abcd in abcd_vec]
# ntot = sum(nvec)

# A = zeros(T, (ntot, ntot))
# B = zeros(T, (ntot, nu))
# C = zeros(T, (ny, ntot))
# D = zeros(T, (ny, nu))

# inds = -1:0
# for j=1:nu
# for i=1:ny
# k = (j-1)*ny + i

# # states corresponding to the transfer function element (i,j)
# inds = (inds.stop+1):(inds.stop+nvec[k])

# A[inds,inds], B[inds,j:j], C[i:i,inds], D[i:i,j:j] = abcd_vec[k]
# end
# end
if balance
A, B, C = balance_statespace(A, B, C)
end
return StateSpace{TE,T}(A, B, C, D, TE(G.timeevol))
end

siso_tf_to_ss(T::Type, f::SisoTf) = siso_tf_to_ss(T, convert(SisoRational, f))
# siso_tf_to_ss(T::Type, f::SisoTf) = siso_tf_to_ss(T, convert(SisoRational, f))

# Conversion to statespace on controllable canonical form
function siso_tf_to_ss(T::Type, f::SisoRational)
# function siso_tf_to_ss(T::Type, f::SisoRational)

num0, den0 = numvec(f), denvec(f)
# Normalize the numerator and denominator to allow realization of transfer functions
# that are proper, but not strictly proper
num = num0 ./ den0[1]
den = den0 ./ den0[1]
# num0, den0 = numvec(f), denvec(f)
# # Normalize the numerator and denominator to allow realization of transfer functions
# # that are proper, but not strictly proper
# num = num0 ./ den0[1]
# den = den0 ./ den0[1]

N = length(den) - 1 # The order of the rational function f
# N = length(den) - 1 # The order of the rational function f

# Get numerator coefficient of the same order as the denominator
bN = length(num) == N+1 ? num[1] : zero(eltype(num))
# # Get numerator coefficient of the same order as the denominator
# bN = length(num) == N+1 ? num[1] : zero(eltype(num))

@views if N == 0 #|| num == zero(Polynomial{T})
A = zeros(T, 0, 0)
B = zeros(T, 0, 1)
C = zeros(T, 1, 0)
else
A = diagm(1 => ones(T, N-1))
A[end, :] .= .-reverse(den)[1:end-1]
# @views if N == 0 #|| num == zero(Polynomial{T})
# A = zeros(T, 0, 0)
# B = zeros(T, 0, 1)
# C = zeros(T, 1, 0)
# else
# A = diagm(1 => ones(T, N-1))
# A[end, :] .= .-reverse(den)[1:end-1]

B = zeros(T, N, 1)
B[end] = one(T)
# B = zeros(T, N, 1)
# B[end] = one(T)

C = zeros(T, 1, N)
C[1:min(N, length(num))] = reverse(num)[1:min(N, length(num))]
C[:] .-= bN .* reverse(den)[1:end-1] # Can index into polynomials at greater inddices than their length
end
D = fill(bN, 1, 1)
# C = zeros(T, 1, N)
# C[1:min(N, length(num))] = reverse(num)[1:min(N, length(num))]
# C[:] .-= bN .* reverse(den)[1:end-1] # Can index into polynomials at greater inddices than their length
# end
# D = fill(bN, 1, 1)

return A, B, C, D
end
# return A, B, C, D
# end

"""
A, B, C, T = balance_statespace{S}(A::Matrix{S}, B::Matrix{S}, C::Matrix{S}, perm::Bool=false)
Expand Down
2 changes: 1 addition & 1 deletion lib/ControlSystemsBase/test/test_complex.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ s = tf("s");
@test tzeros(ss([-1.0-im 1-im; 2 0], [2; 0], [-1+1im -0.5-1.25im], 1)) [-1-2im, 2-im]

@test tzeros(ss((s-2.0-1.5im)^3/(s+1+im)/(s+2)^3)) fill(2.0 + 1.5im, 3) rtol=1e-4
@test tzeros(ss((s-2.0-1.5im)*(s-3.0)/(s+1+im)/(s+2)^2)) [3.0, 2.0 + 1.5im] rtol=1e-14
@test tzeros(ss((s-2.0-1.5im)*(s-3.0)/(s+1+im)/(s+2)^2)) [2.0 + 1.5im, 3.0] rtol=1e-14

end
4 changes: 2 additions & 2 deletions lib/ControlSystemsBase/test/test_conversion.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ A = randn(ComplexF64,3,3)

G = tf(1.0,[1,1])
H = zpk([0.0], [1.0], 1.0)
@inferred ControlSystemsBase.siso_tf_to_ss(Float64, G.matrix[1,1])
@inferred ControlSystemsBase.siso_tf_to_ss(Float64, H.matrix[1,1])
# @inferred ControlSystemsBase.siso_tf_to_ss(Float64, G.matrix[1,1])
# @inferred ControlSystemsBase.siso_tf_to_ss(Float64, H.matrix[1,1])

# Easy second order system
sys1 = ss([-1 0;1 1],[1;0],[1 1],0)
Expand Down
14 changes: 14 additions & 0 deletions lib/ControlSystemsBase/test/test_implicit_diff.jl
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,17 @@ J2 = fdgrad(difffun, pars)[:]
# @show norm(J1-J2)
@test J1 J2 rtol = 1e-5

## Test differentiation of tf 2 ss conversion

function difffun(pars)
P = tf(1, pars)
sum(abs2, step(P, 0:0.1:10, method=:tustin).y +
impulse(P, 0:0.1:10, method=:tustin).y
)
end

pars = [1.0, 2, 1]

g1 = ForwardDiff.gradient(difffun, pars)
g2 = fdgrad(difffun, pars)
@test g1 g2 rtol = 1e-5
4 changes: 2 additions & 2 deletions lib/ControlSystemsBase/test/test_timeresp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ xreal = zeros(3, length(t0), 2)
y, t, x = step(systf, t0, method=:zoh) # Method is :zoh so discretization is applied
yreal[1,:,1] = 1 .- exp.(-t)
yreal[2,:,2] = -1 .+ exp.(-t) + 2*exp.(-t).*t
@test y yreal atol=1e-14
@test y yreal atol=1e-13
#Step ss
y, t, x = step(sysss, t, method=:zoh)
@test y yreal atol=1e-13
Expand All @@ -160,7 +160,7 @@ xreal[3,:,2] = exp.(-t).*(-t .- 1) .+ 1
y, t, x = impulse(systf, t, method=:zoh)
yreal[1,:,1] = exp.(-t)
yreal[2,:,2] = exp.(-t).*(1 .- 2*t)
@test y yreal atol=1e-14
@test y yreal atol=1e-13
#Impulse ss
y, t, x = impulse(1.0sysss, t, method=:zoh)
@test y yreal atol=1e-13
Expand Down
4 changes: 2 additions & 2 deletions test/test_timeresp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ xreal[3,:,2] = exp.(-t).*t
y, t, x = step(systf, t0, method=:zoh)
yreal[1,:,1] = 1 .- exp.(-t)
yreal[2,:,2] = -1 .+ exp.(-t) + 2*exp.(-t).*t
@test y yreal atol=1e-14
@test y yreal atol=1e-13
#Step ss
y, t, x = step(sysss, t, method=:zoh)
@test y yreal atol=1e-13
Expand All @@ -88,7 +88,7 @@ xreal[3,:,2] = exp.(-t).*(-t .- 1) .+ 1
y, t, x = impulse(systf, t, method=:zoh)
yreal[1,:,1] = exp.(-t)
yreal[2,:,2] = exp.(-t).*(1 .- 2*t)
@test y yreal atol=1e-14
@test y yreal atol=1e-13
#Impulse ss
y, t, x = impulse(1.0sysss, t, method=:zoh)
@test y yreal atol=1e-13
Expand Down

0 comments on commit c1cf26a

Please sign in to comment.