diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0cba4307..4abf580de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - master + - dev push: branches: - master diff --git a/README.md b/README.md index 03f446e4e..4a6fa2ed2 100644 --- a/README.md +++ b/README.md @@ -30,31 +30,18 @@ using Pkg; Pkg.add("ControlSystems") More details under [releases](https://github.com/JuliaControl/ControlSystems.jl/releases). -### 2020-10 -- `lsimplot, stepplot, impulseplot` now have the same signatures as the corresponding non-plotting function. -- New function `d2c` for conversion from discrete to continuous. +### 2021-11 +- Time-domain simuations now return a result structure (non breaking) +- *Breaking*: `lsimplot, stepplot, impulseplot` have been replaced by `plot(lsim())` etc. +- *Breaking*: `pole, tzero` has been renamed to their plural form, `poles, tzeros`. +- *Breaking*: `c2d` now no longer returns the `x0map` for statespace systems, see function `c2d_x0map` for the old behavior. +- *Breaking*: The array layout of time and frequency responses has been transposed, i.e., in `y,t,x,u = lsim(sys, ...)`, the output arrays `y,x,u` are now of shape `size(y) == (sys.ny, T)`. +- New functions `observer_controller, observer_predictor, placePI`. +- *Breaking*: The type `LQG` has been removed, see [RobustAndOptimalControl.jl](https://github.com/JuliaControl/RobustAndOptimalControl.jl/blob/master/src/lqg.jl) for its replacement. +- *Breaking*: `balreal` and `baltrunc` return an additional value, the applied similarity transform. +- A large set of bug fixes +- For a full list of changes, [see here](https://github.com/JuliaControl/ControlSystems.jl/pull/565/commits). -### 2020-09-24 -Release v0.7 introduces a new `TimeEvolution` type to handle `Discrete/Continuous` systems. See the [release notes](https://github.com/JuliaControl/ControlSystems.jl/releases/tag/v0.7.0). - -### 2019-11-03 -- Poles and zeros are "not sorted" as in Julia versions < 1.2, even on newer versions of Julia. This should imply that complex conjugates are kept together. - -### 2019-05-28 -#### Delay systems -- We now support systems with time delays. Example: -```julia -sys = tf(1, [1,1])*delay(1) -stepplot(sys, 5) # Compilation time might be long for first simulation -nyquistplot(sys) -``` -#### New examples -- [Delayed systems (frequency domain)](https://github.com/JuliaControl/ControlSystems.jl/blob/master/example/delayed_lti_system.jl) -- [Delayed systems (time domain)](https://github.com/JuliaControl/ControlSystems.jl/blob/master/example/delayed_lti_timeresp.jl) -- [Systems with uncertainty](https://github.com/baggepinnen/MonteCarloMeasurements.jl/blob/master/examples/controlsystems.jl) -- [Robust PID optimization](https://github.com/baggepinnen/MonteCarloMeasurements.jl/blob/master/examples/robust_controller_opt.jl) -### 2019-05-22 -New state-space type `HeteroStateSpace` that accepts matrices of heterogeneous types: [example using `StaticArrays`](https://juliacontrol.github.io/ControlSystems.jl/latest/man/creating_systems/#Creating-State-Space-Systems-1). ## Documentation @@ -66,7 +53,7 @@ Some of the available commands are: ##### Constructing systems ss, tf, zpk ##### Analysis -pole, tzero, norm, hinfnorm, linfnorm, ctrb, obsv, gangoffour, margin, markovparam, damp, dampreport, zpkdata, dcgain, covar, gram, sigma, sisomargin +poles, tzeros, norm, hinfnorm, linfnorm, ctrb, obsv, gangoffour, margin, markovparam, damp, dampreport, zpkdata, dcgain, covar, gram, sigma, sisomargin ##### Synthesis care, dare, dlyap, lqr, dlqr, place, leadlink, laglink, leadlinkat, rstd, rstc, dab, balreal, baltrunc ###### PID design diff --git a/docs/src/examples/example.md b/docs/src/examples/example.md index d7843cd76..0001672da 100644 --- a/docs/src/examples/example.md +++ b/docs/src/examples/example.md @@ -1,6 +1,6 @@ ```@meta DocTestSetup = quote - using ControlSystems + using ControlSystems, Plots plotsDir = joinpath(dirname(pathof(ControlSystems)), "..", "docs", "build", "plots") mkpath(plotsDir) save_docs_plot(name) = Plots.savefig(joinpath(plotsDir,name)) @@ -25,7 +25,7 @@ u(x,t) = -L*x .+ 1.5(t>=2.5)# Form control law (u is a function of t and x), a t =0:Ts:5 x0 = [1,0] y, t, x, uout = lsim(sys,u,t,x0=x0) -Plots.plot(t,x, lab=["Position" "Velocity"], xlabel="Time [s]") +Plots.plot(t,x', lab=["Position" "Velocity"], xlabel="Time [s]") save_docs_plot("lqrplot.svg"); # hide @@ -132,8 +132,8 @@ R,S,T = rstc(B⁺,B⁻,A,Bm,Am,Ao,AR) # Calculate the 2-DOF controller polynomia Gcl = tf(conv(B,T),zpconv(A,R,B,S)) # Form the closed loop polynomial from reference to output, the closed-loop characteristic polynomial is AR + BS, the function zpconv takes care of the polynomial multiplication and makes sure the coefficient vectores are of equal length -stepplot(P) -stepplot!(Gcl) # Visualize the open and closed loop responses. +plot(step(P)) +plot!(step(Gcl)) # Visualize the open and closed loop responses. save_docs_plot("ppstepplot.svg") # hide gangoffourplot(P, tf(-S,R)) # Plot the gang of four to check that all tranfer functions are OK save_docs_plot("ppgofplot.svg"); # hide diff --git a/docs/src/index.md b/docs/src/index.md index 1e25f44cc..c7bf394db 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,6 +2,12 @@ ```@meta CurrentModule = ControlSystems +DocTestFilters = [ + r"StateSpace.+?\n" + r"HeteroStateSpace.+?\n" + r"TransferFunction.+?\n" + r"DelayLtiSystem.+?\n" +] ``` ## Guide diff --git a/docs/src/man/creating_systems.md b/docs/src/man/creating_systems.md index 712864197..abad6487e 100644 --- a/docs/src/man/creating_systems.md +++ b/docs/src/man/creating_systems.md @@ -121,7 +121,7 @@ D = Continuous-time state-space model julia> HeteroStateSpace(sys, to_sized) -HeteroStateSpace{Continuous, SizedMatrix{2, 2, Int64, 2}, SizedMatrix{2, 1, Int64, 2}, SizedMatrix{1, 2, Int64, 2}, SizedMatrix{1, 1, Int64, 2}} +HeteroStateSpace{Continuous, SizedMatrix{2, 2, Int64, 2, Matrix{Int64}}, SizedMatrix{2, 1, Int64, 2, Matrix{Int64}}, SizedMatrix{1, 2, Int64, 2, Matrix{Int64}}, SizedMatrix{1, 1, Int64, 2, Matrix{Int64}}} A = -5 0 0 -5 diff --git a/example/dc_motor_lqg_design.jl b/example/dc_motor_lqg_design.jl index 53857f9c1..19d5430bf 100644 --- a/example/dc_motor_lqg_design.jl +++ b/example/dc_motor_lqg_design.jl @@ -29,7 +29,7 @@ function motor(Ke, Kt, L, R, J, b=1e-3) end p60 = motor(Ke, Kt, L, Rel, J) -f1 = stepplot(p60) +f1 = stepplot(p60, 1) f2 = bodeplot(p60) # LQR control @@ -45,12 +45,20 @@ Nbar = 1. ./ (p60.D - (p60.C - p60.D*K) * inv(p60.A - p60.B*K) * p60.B) Vd = [10. 0 # covariance of the speed estimation 0 10]; # covariance of the current estimation Vn = 0.04; # covariance for the speed measurement (radians/s)^2 -G = LQG(p60, Q, mat(R), Vd, mat(Vn)) -Gcl = G[:cl] -T = G[:T] -S = G[:S]; +Ka = kalman(p60, Vd, mat(Vn)) +C = ControlSystems.observer_controller(p60, K, Ka) + +Gcl = let (A,B,C,D) = ssdata(p60) + Acl = [A-B*K B*K; zero(A) A-Ka*C] + Bcl = [B * Nbar; zero(B)] + Ccl = [C zero(C)] + ss(Acl, Bcl, Ccl, 0) +end + +T = Gcl +S = 1-T # 1000 logarithmically spaced values from -3 to 3 -f3 = sigmaplot([S,T], exp10.(range(-3, stop=3, length=1000))) -f4 = stepplot(Gcl, label=["Closed loop system using LQG"]) +f3 = bodeplot([Gcl, S, T], exp10.(range(-3, stop=3, length=1000))) +f4 = stepplot(Gcl, 1, label="Closed loop system using LQG") Plots.plot(f1, f2, f3, f4, layout=(2,2), size=(800, 600)) diff --git a/src/ControlSystems.jl b/src/ControlSystems.jl index 244583d16..ffb6f762e 100644 --- a/src/ControlSystems.jl +++ b/src/ControlSystems.jl @@ -11,7 +11,6 @@ export LTISystem, ss, tf, zpk, - LQG, isproper, # Linear Algebra balance, @@ -32,7 +31,6 @@ export LTISystem, ctrb, obsv, place, - luenberger, # Model Simplification reduce_sys, sminreal, @@ -42,10 +40,12 @@ export LTISystem, similarity_transform, prescale, innovation_form, + observer_predictor, + observer_controller, # Stability Analysis isstable, - pole, - tzero, + poles, + tzeros, dcgain, zpkdata, damp, @@ -115,6 +115,8 @@ using MacroTools abstract type AbstractSystem end + +include("types/result_types.jl") include("types/TimeEvolution.jl") ## Added interface: # timeevol(Lti) -> TimeEvolution (not exported) @@ -141,8 +143,6 @@ include("types/DelayLtiSystem.jl") include("types/tf.jl") include("types/zpk.jl") -include("types/lqg.jl") # QUESTION: is it really motivated to have an LQG type? - include("utilities.jl") include("types/promotion.jl") @@ -169,10 +169,14 @@ include("delay_systems.jl") include("plotting.jl") +@deprecate pole poles +@deprecate tzero tzeros @deprecate num numvec @deprecate den denvec @deprecate norminf hinfnorm @deprecate diagonalize(s::AbstractStateSpace, digits) diagonalize(s::AbstractStateSpace) +@deprecate luenberger(sys, p) place(sys, p, :o) +@deprecate luenberger(A, C, p) place(A, C, p, :o) # There are some deprecations in pid_control.jl for laglink/leadlink/leadlinkat function covar(D::Union{AbstractMatrix,UniformScaling}, R) diff --git a/src/analysis.jl b/src/analysis.jl index 3c45681f8..c6437d576 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -1,20 +1,20 @@ """ - pole(sys) + poles(sys) Compute the poles of system `sys`.""" -pole(sys::AbstractStateSpace) = eigvalsnosort(sys.A) -pole(sys::SisoTf) = error("pole is not implemented for type $(typeof(sys))") +poles(sys::AbstractStateSpace) = eigvalsnosort(sys.A) +poles(sys::SisoTf) = error("pole is not implemented for type $(typeof(sys))") # Seems to have a lot of rounding problems if we run the full thing with sisorational, # converting to zpk before works better in the cases I have tested. -pole(sys::TransferFunction) = pole(zpk(sys)) +poles(sys::TransferFunction) = poles(zpk(sys)) -function pole(sys::TransferFunction{<:TimeEvolution,SisoZpk{T,TR}}) where {T, TR} +function poles(sys::TransferFunction{<:TimeEvolution,SisoZpk{T,TR}}) where {T, TR} # With right TR, this code works for any SisoTf # Calculate least common denominator of the minors, # i.e. something like least common multiple of the pole-polynomials - individualpoles = [map(pole, sys.matrix)...;] + individualpoles = [map(poles, sys.matrix)...;] lcmpoles = TR[] for poles = minorpoles(sys.matrix) # Poles have to be equal to existing poles for the individual transfer functions and this @@ -45,9 +45,9 @@ function minorpoles(sys::Matrix{SisoZpk{T, TR}}) where {T, TR} minors = Array{TR,1}[] ny, nu = size(sys) if ny == nu == 1 - push!(minors, pole(sys[1, 1])) + push!(minors, poles(sys[1, 1])) elseif ny == nu - push!(minors, pole(det(sys))) + push!(minors, poles(det(sys))) for i = 1:ny for j = 1:nu newmat = sys[1:end .!=i, 1:end .!= j] @@ -75,7 +75,7 @@ Compute the determinant of the Matrix `sys` of SisoTf systems, returns a SisoTf # TODO: improve this implementation, should be more efficient ones function det(sys::Matrix{S}) where {S<:SisoZpk} ny, nu = size(sys) - @assert ny == nu "Matrix is not square" + ny == nu || throw(ArgumentError("sys matrix is not square")) if ny == 1 return sys[1, 1] end @@ -89,13 +89,17 @@ function det(sys::Matrix{S}) where {S<:SisoZpk} end """ - dcgain(sys) + dcgain(sys, ϵ=0) Compute the dcgain of system `sys`. -equal to G(0) for continuous-time systems and G(1) for discrete-time systems.""" -function dcgain(sys::LTISystem) - return iscontinuous(sys) ? evalfr(sys, 0) : evalfr(sys, 1) +equal to G(0) for continuous-time systems and G(1) for discrete-time systems. + +`ϵ` can be provided to evaluate the dcgain with a small perturbation into +the stability region of the complex plane. +""" +function dcgain(sys::LTISystem, ϵ=0) + return iscontinuous(sys) ? evalfr(sys, -ϵ) : evalfr(sys, exp(-ϵ*sys.Ts)) end """ @@ -139,9 +143,9 @@ end Compute the natural frequencies, `Wn`, and damping ratios, `zeta`, of the poles, `ps`, of `sys`""" function damp(sys::LTISystem) - ps = pole(sys) + ps = poles(sys) if isdiscrete(sys) - ps = log.(ps)/sys.Ts + ps = log.(complex.(ps))/sys.Ts end Wn = abs.(ps) order = sortperm(Wn; by=z->(abs(z), real(z), imag(z))) @@ -160,20 +164,29 @@ function dampreport(io::IO, sys::LTISystem) Wn, zeta, ps = damp(sys) t_const = 1 ./ (Wn.*zeta) header = - ("| Pole | Damping | Frequency | Time Constant |\n"* - "| | Ratio | (rad/sec) | (sec) |\n"* - "+---------------+---------------+---------------+---------------+") + ("| Pole | Damping | Frequency | Frequency | Time Constant |\n"* + "| | Ratio | (rad/sec) | (Hz) | (sec) |\n"* + "+--------------------+---------------+---------------+---------------+---------------+") println(io, header) if all(isreal, ps) for i=eachindex(ps) p, z, w, t = ps[i], zeta[i], Wn[i], t_const[i] - Printf.@printf(io, "| %-13.3e| %-13.3e| %-13.3e| %-13.3e|\n", real(p), z, w, t) + Printf.@printf(io, "| %-+18.3g | %-13.3g| %-13.3g| %-13.3g| %-13.3g|\n", real(p), z, w, w/(2π), t) end - else + elseif numeric_type(sys) <: Real # real-coeff system with complex conj. poles for i=eachindex(ps) p, z, w, t = ps[i], zeta[i], Wn[i], t_const[i] - Printf.@printf(io, "| %-13.3e| %-13.3e| %-13.3e| %-13.3e|\n", real(p), z, w, t) - Printf.@printf(io, "| %-+11.3eim| | | |\n", imag(p)) + imag(p) < 0 && (continue) # use only the positive complex pole to print with the ± operator + if imag(p) == 0 # no ± operator for real pole + Printf.@printf(io, "| %-+18.3g | %-13.3g| %-13.3g| %-13.3g| %-13.3g|\n", real(p), z, w, w/(2π), t) + else + Printf.@printf(io, "| %-+7.3g ± %6.3gim | %-13.3g| %-13.3g| %-13.3g| %-13.3g|\n", real(p), imag(p), z, w, w/(2π), t) + end + end + else # complex-coeff system + for i=eachindex(ps) + p, z, w, t = ps[i], zeta[i], Wn[i], t_const[i] + Printf.@printf(io, "| %-+7.3g %+7.3gim | %-13.3g| %-13.3g| %-13.3g| %-13.3g|\n", real(p), imag(p), z, w, w/(2π), t) end end end @@ -181,15 +194,15 @@ dampreport(sys::LTISystem) = dampreport(stdout, sys) """ - tzero(sys) + tzeros(sys) Compute the invariant zeros of the system `sys`. If `sys` is a minimal realization, these are also the transmission zeros.""" -function tzero(sys::TransferFunction) +function tzeros(sys::TransferFunction) if issiso(sys) - return tzero(sys.matrix[1,1]) + return tzeros(sys.matrix[1,1]) else - return tzero(ss(sys)) + return tzeros(ss(sys)) end end @@ -198,14 +211,14 @@ end # Multivariable Systems," Automatica, 18 (1982), pp. 415–430. # # Note that this returns either Vector{ComplexF32} or Vector{Float64} -tzero(sys::AbstractStateSpace) = tzero(sys.A, sys.B, sys.C, sys.D) +tzeros(sys::AbstractStateSpace) = tzeros(sys.A, sys.B, sys.C, sys.D) # Make sure everything is BlasFloat -function tzero(A::AbstractMatrix, B::AbstractMatrix, C::AbstractMatrix, D::AbstractMatrix) +function tzeros(A::AbstractMatrix, B::AbstractMatrix, C::AbstractMatrix, D::AbstractMatrix) T = promote_type(eltype(A), eltype(B), eltype(C), eltype(D)) A2, B2, C2, D2, _ = promote(A,B,C,D, fill(zero(T)/one(T),0,0)) # If Int, we get Float64 - tzero(A2, B2, C2, D2) + tzeros(A2, B2, C2, D2) end -function tzero(A::AbstractMatrix{T}, B::AbstractMatrix{T}, C::AbstractMatrix{T}, D::AbstractMatrix{T}) where {T <: Union{AbstractFloat,Complex{<:AbstractFloat}}#= For eps(T) =#} +function tzeros(A::AbstractMatrix{T}, B::AbstractMatrix{T}, C::AbstractMatrix{T}, D::AbstractMatrix{T}) where {T <: Union{AbstractFloat,Complex{<:AbstractFloat}}#= For eps(T) =#} # Balance the system A, B, C = balance_statespace(A, B, C) @@ -462,7 +475,7 @@ function delaymargin(G::LTISystem) end function robust_minreal(G, args...; kwargs...) - try + try return minreal(G, args...; kwargs...) catch return G @@ -470,42 +483,41 @@ function robust_minreal(G, args...; kwargs...) end """ - S,D,N,T = gangoffour(P,C; minimal=true) - gangoffour(P::AbstractVector,C::AbstractVector; minimal=true) - -Given a transfer function describing the Plant `P` and a transfer function describing the controller `C`, computes the four transfer functions in the Gang-of-Four. + S, PS, CS, T = gangoffour(P, C; minimal=true) + gangoffour(P::AbstractVector, C::AbstractVector; minimal=true) + +Given a transfer function describing the plant `P` and a transfer function describing the controller `C`, computes the four transfer functions in the Gang-of-Four. -- `minimal` determines whether or not to call `minreal` on the computed systems. - `S = 1/(1+PC)` Sensitivity function -- `D = P/(1+PC)` -- `N = C/(1+PC)` +- `PS = P/(1+PC)` Load disturbance to measurement signal +- `CS = C/(1+PC)` Measurement noise to control signal - `T = PC/(1+PC)` Complementary sensitivity function Only supports SISO systems """ -function gangoffour(P::LTISystem,C::LTISystem; minimal=true) - if P.nu + P.ny + C.nu + C.ny > 4 +function gangoffour(P::LTISystem, C::LTISystem; minimal=true) + if !issiso(P) || !issiso(C) error("gangoffour only supports SISO systems") end minfun = minimal ? robust_minreal : identity - S = (1/(1+P*C)) |> minfun - D = (P*S) |> minfun - N = (C*S) |> minfun - T = (P*N) |> minfun - return S, D, N, T + S = feedback(1, P*C) |> minfun + PS = feedback(P, C) |> minfun + CS = feedback(C, P) |> minfun + T = feedback(P*C, 1) |> minfun + return S, PS, CS, T end """ - S, D, N, T, RY, RU, RE = gangofseven(P,C,F) + S, PS, CS, T, RY, RU, RE = gangofseven(P,C,F) Given transfer functions describing the Plant `P`, the controller `C` and a feed forward block `F`, computes the four transfer functions in the Gang-of-Four and the transferfunctions corresponding to the feed forward. `S = 1/(1+PC)` Sensitivity function -`D = P/(1+PC)` +`PS = P/(1+PC)` -`N = C/(1+PC)` +`CS = C/(1+PC)` `T = PC/(1+PC)` Complementary sensitivity function @@ -516,13 +528,13 @@ computes the four transfer functions in the Gang-of-Four and the transferfunctio `RE = F/(1+P*C)` Only supports SISO systems""" -function gangofseven(P::TransferFunction,C::TransferFunction,F::TransferFunction) - if P.nu + P.ny + C.nu + C.ny + F.nu + F.ny > 6 - error("gof only supports SISO systems") +function gangofseven(P::TransferFunction, C::TransferFunction, F::TransferFunction) + if !issiso(P) || !issiso(C) || !issiso(F) + error("gangofseven only supports SISO systems") end - S, D, N, T = gangoffour(P,C) + S, PS, CS, T = gangoffour(P,C) RY = T*F - RU = N*F + RU = CS*F RE = S*F - return S, D, N, T, RY, RU, RE + return S, PS, CS, T, RY, RU, RE end diff --git a/src/connections.jl b/src/connections.jl index 769ad1669..0b63f351b 100644 --- a/src/connections.jl +++ b/src/connections.jl @@ -151,16 +151,34 @@ function blockdiag(mats::AbstractMatrix{T}...) where T end - """ - feedback(L) - feedback(P1,P2) + feedback(sys) + feedback(sys1, sys2) -Returns `L/(1+L)` or `P1/(1+P1*P2)` +For a general LTI-system, `feedback` forms the negative feedback interconnection +```julia +>-+ sys1 +--> + | | + (-)sys2 + +``` +If no second system is given, negative identity feedback is assumed """ feedback(L::TransferFunction) = L/(1+L) feedback(P1::TransferFunction, P2::TransferFunction) = P1/(1+P1*P2) +function feedback(G1::TransferFunction{<:TimeEvolution,<:SisoRational}, G2::TransferFunction{<:TimeEvolution,<:SisoRational}) + if !issiso(G1) || !issiso(G2) + error("MIMO TransferFunction feedback isn't implemented.") + end + G1num = numpoly(G1)[] + G1den = denpoly(G1)[] + G2num = numpoly(G2)[] + G2den = denpoly(G2)[] + + timeevol = common_timeevol(G1, G2) + tf(G1num*G2den, G1num*G2num + G1den*G2den, timeevol) +end + #Efficient implementations function feedback(L::TransferFunction{<:TimeEvolution,T}) where T<:SisoRational if size(L) != (1,1) @@ -184,48 +202,53 @@ function feedback(L::TransferFunction{TE, T}) where {TE<:TimeEvolution, T<:SisoZ return TransferFunction{TE,T}(fill(sisozpk,1,1), L.timeevol) end -""" - feedback(sys) - feedback(sys1,sys2) - -Forms the negative feedback interconnection -```julia ->-+ sys1 +--> - | | - (-)sys2 + -``` -If no second system is given, negative identity feedback is assumed -""" -function feedback(sys::Union{StateSpace, DelayLtiSystem}) +function feedback(sys::Union{AbstractStateSpace, DelayLtiSystem}) ninputs(sys) != noutputs(sys) && error("Use feedback(sys1, sys2) if number of inputs != outputs") - feedback(sys,ss(Matrix{numeric_type(sys)}(I,size(sys)...))) -end - -function feedback(sys1::StateSpace,sys2::StateSpace) - timeevol = common_timeevol(sys1,sys2) - !(iszero(sys1.D) || iszero(sys2.D)) && error("There cannot be a direct term (D) in both sys1 and sys2") - A = [sys1.A+sys1.B*(-sys2.D)*sys1.C sys1.B*(-sys2.C); - sys2.B*sys1.C sys2.A+sys2.B*sys1.D*(-sys2.C)] - B = [sys1.B; sys2.B*sys1.D] - C = [sys1.C sys1.D*(-sys2.C)] - ss(A, B, C, sys1.D, timeevol) + feedback(sys,ss(Matrix{numeric_type(sys)}(I,size(sys)...), sys.timeevol)) end - """ - feedback(s1::AbstractStateSpace, s2::AbstractStateSpace; + feedback(sys1::AbstractStateSpace, sys2::AbstractStateSpace; U1=:, Y1=:, U2=:, Y2=:, W1=:, Z1=:, W2=Int[], Z2=Int[], Wperm=:, Zperm=:, pos_feedback::Bool=false) +*Basic use* `feedback(sys1, sys2)` forms the feedback interconnection +```julia + ┌──────────────┐ +◄──────────┤ sys1 │◄──── Σ ◄────── + │ │ │ │ + │ └──────────────┘ -1 + │ | + │ ┌──────────────┐ │ + └─────►│ sys2 ├──────┘ + │ │ + └──────────────┘ +``` +*Advanced use* +`feedback` also supports more flexible use according to the figure below +```julia + ┌──────────────┐ + z1◄─────┤ sys1 │◄──────w1 + ┌─── y1◄─────┤ │◄──────u1 ◄─┐ + │ └──────────────┘ │ + │ α + │ ┌──────────────┐ │ + └──► u2─────►│ sys2 ├───────►y2──┘ + w2─────►│ ├───────►z2 + └──────────────┘ +``` +`U1`, `W1` specifies the indices of the input signals of `sys1` corresponding to `u1` and `w1` +`Y1`, `Z1` specifies the indices of the output signals of `sys1` corresponding to `y1` and `z1` +`U2`, `W2`, `Y2`, `Z2` specifies the corresponding signals of `sys2` -`U1`, `Y1`, `U2`, `Y2` contain the indices of the signals that should be connected. -`W1`, `Z1`, `W2`, `Z2` contain the signal indices of `s1` and `s2` that should be kept. +Specify `Wperm` and `Zperm` to reorder the inputs (corresponding to [w1; w2]) +and outputs (corresponding to [z1; z2]) in the resulting statespace model. -Specify `Wperm` and `Zperm` to reorder [w1; w2] and [z1; z2] in the resulting statespace model. +Negative feedback (α = -1) is the default. Specify `pos_feedback=true` for positive feedback (α = 1). -Negative feedback is the default. Specify `pos_feedback=true` for positive feedback. +See also `lft`, `starprod`. -See Zhou etc. for similar (somewhat less symmetric) formulas. +See Zhou, Doyle, Glover (1996) for similar (somewhat less symmetric) formulas. """ @views function feedback(sys1::AbstractStateSpace, sys2::AbstractStateSpace; U1=:, Y1=:, U2=:, Y2=:, W1=:, Z1=:, W2=Int[], Z2=Int[], @@ -324,7 +347,7 @@ feedback2dof(B,A,R,S,T) = tf(conv(B,T),zpconv(A,R,B,S)) Return the transfer function `P(F+C)/(1+PC)` which is the closed-loop system with process `P`, controller `C` and feedforward filter `F` from reference to control signal (by-passing `C`). - +``` +-------+ | | +-----> F +----+ @@ -337,6 +360,7 @@ r | - | | | | | y | +-------+ +-------+ | | | +--------------------------------+ +``` """ function feedback2dof(P::TransferFunction{TE}, C::TransferFunction{TE}, F::TransferFunction{TE}) where TE !issiso(P) && error("Feedback not implemented for MIMO systems") diff --git a/src/delay_systems.jl b/src/delay_systems.jl index 899b0f62b..ae0bc94e5 100644 --- a/src/delay_systems.jl +++ b/src/delay_systems.jl @@ -180,7 +180,7 @@ function _lsim(sys::DelayLtiSystem{T,S}, Base.@nospecialize(u!), t::AbstractArra p = (A, B1, B2, C1, C2, D11, D12, D21, D22, y, Tau, u!, uout, hout, tmpy, t) # This callback computes and stores the delay term - sv = SavedValues(Float64, Tuple{Vector{Float64},Vector{Float64}}) + sv = SavedValues(eltype(t), Tuple{Vector{T},Vector{T}}) cb = SavingCallback(dde_saver, sv, saveat = t) # History function, only used for d @@ -204,7 +204,7 @@ function _lsim(sys::DelayLtiSystem{T,S}, Base.@nospecialize(u!), t::AbstractArra y[:,k] .= sv.saveval[k][2] end - return y', t, x' + return y, t, x end # We have to default to something, look at the sys.P.P and delays @@ -224,7 +224,7 @@ function _default_dt(sys::DelayLtiSystem) if !isstable(sys.P.P) return 0.05 # Something small else - ps = pole(sys.P.P) + ps = poles(sys.P.P) r = minimum([abs.(real.(ps));0]) # Find the fastest pole of sys.P.P r = min(r, minimum([sys.Tau;0])) # Find the fastest delay if r == 0.0 @@ -244,8 +244,8 @@ function Base.step(sys::DelayLtiSystem{T}, t::AbstractVector; kwargs...) where T if nu == 1 y, tout, x = lsim(sys, u, t; x0=x0, kwargs...) else - x = Array{T}(undef, length(t), nstates(sys), nu) - y = Array{T}(undef, length(t), noutputs(sys), nu) + x = Array{T}(undef, nstates(sys), length(t), nu) + y = Array{T}(undef, noutputs(sys), length(t), nu) for i=1:nu y[:,:,i], tout, x[:,:,i] = lsim(sys[:,i], u, t; x0=x0, kwargs...) end @@ -256,7 +256,8 @@ end function impulse(sys::DelayLtiSystem{T}, t::AbstractVector; alg=MethodOfSteps(BS3()), kwargs...) where T nu = ninputs(sys) - iszero(sys.P.D12) || @warn("Impulse with a direct term from input to delay vector leads to poor accuracy.") + iszero(sys.P.D22) || @warn "Impulse with a direct term from delay vector to delay vector can lead to poor results." maxlog=10 + iszero(sys.P.D21) || throw(ArgumentError("Impulse with a direct term from input to delay vector is not implemented. Move the delays to the output instead of input if possible.")) if t[1] != 0 throw(ArgumentError("First time point must be 0 in impulse")) end @@ -264,8 +265,8 @@ function impulse(sys::DelayLtiSystem{T}, t::AbstractVector; alg=MethodOfSteps(BS if nu == 1 y, tout, x = lsim(sys, u, t; alg=alg, x0=sys.P.B[:,1], kwargs...) else - x = Array{T}(undef, length(t), nstates(sys), nu) - y = Array{T}(undef, length(t), noutputs(sys), nu) + x = Array{T}(undef, nstates(sys), length(t), nu) + y = Array{T}(undef, noutputs(sys), length(t), nu) for i=1:nu y[:,:,i], tout, x[:,:,i] = lsim(sys[:,i], u, t; alg=alg, x0=sys.P.B[:,i], kwargs...) end diff --git a/src/discrete.jl b/src/discrete.jl index ae0a0237f..dd843e0dc 100644 --- a/src/discrete.jl +++ b/src/discrete.jl @@ -2,27 +2,31 @@ export rstd, rstc, dab, c2d_roots2poly, c2d_poly2poly, zpconv#, lsima, indirect_ """ - sysd = c2d(sys::AbstractStateSpace{<:Continuous}, Ts, method=:zoh) + sysd = c2d(sys::AbstractStateSpace{<:Continuous}, Ts, method=:zoh; w_prewarp=0) Gd = c2d(G::TransferFunction{<:Continuous}, Ts, method=:zoh) Convert the continuous-time system `sys` into a discrete-time system with sample time -`Ts`, using the specified `method` (:`zoh`, `:foh` or `:fwdeuler`). +`Ts`, using the specified `method` (:`zoh`, `:foh`, `:fwdeuler` or `:tustin`). Note that the forward-Euler method generally requires the sample time to be very small relative to the time constants of the system. +`method = :tustin` performs a bilinear transform with prewarp frequency `w_prewarp`. + +- `w_prewarp`: Frequency (rad/s) for pre-warping when usingthe Tustin method, has no effect for other methods. + See also `c2d_x0map` """ -c2d(sys::AbstractStateSpace{<:Continuous}, Ts::Real, method::Symbol=:zoh) = c2d_x0map(sys, Ts, method)[1] +c2d(sys::AbstractStateSpace{<:Continuous}, Ts::Real, method::Symbol=:zoh; kwargs...) = c2d_x0map(sys, Ts, method; kwargs...)[1] """ - sysd, x0map = c2d_x0map(sys::AbstractStateSpace{<:Continuous}, Ts, method=:zoh) + sysd, x0map = c2d_x0map(sys::AbstractStateSpace{<:Continuous}, Ts, method=:zoh; w_prewarp=0) Returns the discretization `sysd` of the system `sys` and a matrix `x0map` that transforms the initial conditions to the discrete domain by `x0_discrete = x0map*[x0; u0]` See `c2d` for further details.""" -function c2d_x0map(sys::AbstractStateSpace{<:Continuous}, Ts::Real, method::Symbol=:zoh) +function c2d_x0map(sys::AbstractStateSpace{<:Continuous}, Ts::Real, method::Symbol=:zoh; w_prewarp=0) A, B, C, D = ssdata(sys) T = promote_type(eltype.((A,B,C,D))...) ny, nu = size(sys) @@ -49,7 +53,16 @@ function c2d_x0map(sys::AbstractStateSpace{<:Continuous}, Ts::Real, method::Symb elseif method === :fwdeuler Ad, Bd, Cd, Dd = (I+Ts*A), Ts*B, C, D x0map = I(nx) - elseif method === :tustin || method === :matched + elseif method === :tustin + a = w_prewarp == 0 ? Ts/2 : tan(w_prewarp*Ts/2)/w_prewarp + a > 0 || throw(DomainError("A positive w_prewarp must be provided for method Tustin")) + AI = (I-a*A) + Ad = AI\(I+a*A) + Bd = 2a*(AI\B) + Cd = C/AI + Dd = a*Cd*B + D + x0map = Matrix{T}(I, nx, nx) + elseif method === :matched error("NotImplemented: Only `:zoh`, `:foh` and `:fwdeuler` implemented so far") else error("Unsupported method: ", method) @@ -59,12 +72,14 @@ function c2d_x0map(sys::AbstractStateSpace{<:Continuous}, Ts::Real, method::Symb end """ - d2c(sys::AbstractStateSpace{<:Discrete}, method::Symbol = :zoh) + d2c(sys::AbstractStateSpace{<:Discrete}, method::Symbol = :zoh; w_prewarp=0) Convert discrete-time system to a continuous time system, assuming that the discrete-time system was discretized using `method`. Available methods are `:zoh, :fwdeuler´. + +- `w_prewarp`: Frequency for pre-warping when usingthe Tustin method, has no effect for other methods. """ -function d2c(sys::AbstractStateSpace{<:Discrete}, method::Symbol=:zoh) - A, B, Cc, Dc = ssdata(sys) +function d2c(sys::AbstractStateSpace{<:Discrete}, method::Symbol=:zoh; w_prewarp=0) + A, B, C, D = ssdata(sys) ny, nu = size(sys) nx = nstates(sys) if method === :zoh @@ -75,9 +90,19 @@ function d2c(sys::AbstractStateSpace{<:Discrete}, method::Symbol=:zoh) if eltype(A) <: Real Ac,Bc = real.((Ac, Bc)) end + Cc, Dc = C, D elseif method === :fwdeuler Ac = (A-I)./sys.Ts Bc = B./sys.Ts + Cc, Dc = C, D + elseif method === :tustin + a = w_prewarp == 0 ? sys.Ts/2 : tan(w_prewarp*sys.Ts/2)/w_prewarp + a > 0 || throw(DomainError("A positive w_prewarp must be provided for method Tustin")) + AI = a*(A+I) + Ac = (A-I)/AI + Bc = AI\B + Cc = 2a*C/AI + Dc = D - Cc*B/2 else error("Unsupported method: ", method) end @@ -227,13 +252,16 @@ function c2d_poly2poly(p, Ts) return real(Polynomials.poly(exp(ro .* Ts)).coeffs[end:-1:1]) end +function c2d(G::TransferFunction{<:Continuous, <:SisoRational}, Ts, args...; kwargs...) + issiso(G) || error("c2d(G::TransferFunction, h) not implemented for MIMO systems") + sysd = c2d(ss(G), Ts, args...; kwargs...) + return convert(TransferFunction{typeof(sysd.timeevol), SisoRational}, sysd) +end -function c2d(G::TransferFunction{<:Continuous}, Ts, args...; kwargs...) - ny, nu = size(G) - @assert (ny + nu == 2) "c2d(G::TransferFunction, Ts) not implemented for MIMO systems" - sys = ss(G) - sysd = c2d(sys, Ts, args...; kwargs...) - return convert(TransferFunction, sysd) +function c2d(G::TransferFunction{<:Continuous, <:SisoZpk}, Ts, args...; kwargs...) + issiso(G) || error("c2d(G::TransferFunction, h) not implemented for MIMO systems") + sysd = c2d(ss(G), Ts, args...; kwargs...) + return convert(TransferFunction{typeof(sysd.timeevol), SisoZpk}, sysd) end """ diff --git a/src/freqresp.jl b/src/freqresp.jl index 0fd7690c5..2adb98953 100644 --- a/src/freqresp.jl +++ b/src/freqresp.jl @@ -12,9 +12,9 @@ of system `sys` over the frequency vector `w`.""" else s_vec = exp.(w_vec*(im*sys.Ts)) end - if isa(sys, StateSpace) - sys = _preprocess_for_freqresp(sys) - end + #if isa(sys, StateSpace) + # sys = _preprocess_for_freqresp(sys) + #end ny,nu = noutputs(sys), ninputs(sys) [evalfr(sys[i,j], s)[] for s in s_vec, i in 1:ny, j in 1:nu] end @@ -57,7 +57,7 @@ function evalfr(sys::AbstractStateSpace, s::Number) R = s*I - sys.A sys.D + sys.C*((R\sys.B)) catch e - @warn "Got exception $e, returning Inf" max_log=1 + @warn "Got exception $e, returning Inf" maxlog=1 fill(convert(T, Inf), size(sys)) end end @@ -79,7 +79,7 @@ function (sys::TransferFunction)(s) end function (sys::TransferFunction)(z_or_omega::Number, map_to_unit_circle::Bool) - @assert isdiscrete(sys) "It only makes no sense to call this function with discrete systems" + isdiscrete(sys) || throw(ArgumentError("It only makes no sense to call this function with discrete systems")) if map_to_unit_circle isreal(z_or_omega) ? evalfr(sys,exp(im*z_or_omega.*sys.Ts)) : error("To map to the unit circle, omega should be real") else @@ -88,7 +88,7 @@ function (sys::TransferFunction)(z_or_omega::Number, map_to_unit_circle::Bool) end function (sys::TransferFunction)(z_or_omegas::AbstractVector, map_to_unit_circle::Bool) - @assert isdiscrete(sys) "It only makes no sense to call this function with discrete systems" + isdiscrete(sys) || throw(ArgumentError("It only makes no sense to call this function with discrete systems")) vals = sys.(z_or_omegas, map_to_unit_circle)# evalfr.(sys,exp.(evalpoints)) # Reshape from vector of evalfr matrizes, to (in,out,freq) Array nu,ny = size(vals[1]) @@ -156,7 +156,7 @@ function _bounds_and_features(sys::LTISystem, plot::Val) zp = zp[imag(zp) .>= 0.0] else # For sigma plots, use the MIMO poles and zeros - zp = [tzero(sys); pole(sys)] + zp = [tzeros(sys); poles(sys)] end # Get the frequencies of the features, ignoring low frequency dynamics fzp = log10.(abs.(zp)) diff --git a/src/matrix_comps.jl b/src/matrix_comps.jl index 50a11a454..46ad326be 100644 --- a/src/matrix_comps.jl +++ b/src/matrix_comps.jl @@ -98,28 +98,31 @@ function gram(sys::AbstractStateSpace, opt::Symbol) end end -"""`obsv(A, C)` or `obsv(sys)` +""" + obsv(A, C, n=size(A,1)) + obsv(sys, n=sys.nx) -Compute the observability matrix for the system described by `(A, C)` or `sys`. +Compute the observability matrix with `n` rows for the system described by `(A, C)` or `sys`. Providing the optional `n > sys.nx` returns an extended observability matrix. Note that checking for observability by computing the rank from `obsv` is not the most numerically accurate way, a better method is checking if -`gram(sys, :o)` is positive definite.""" -function obsv(A::AbstractMatrix, C::AbstractMatrix) +`gram(sys, :o)` is positive definite. +""" +function obsv(A::AbstractMatrix, C::AbstractMatrix, n::Int = size(A,1)) T = promote_type(eltype(A), eltype(C)) - n = size(A, 1) + nx = size(A, 1) ny = size(C, 1) - if n != size(C, 2) + if nx != size(C, 2) throw(ArgumentError("C must have the same number of columns as A")) end - res = fill(zero(T), n*ny, n) + res = fill(zero(T), n*ny, nx) res[1:ny, :] = C for i=1:n-1 res[(1 + i*ny):(1 + i)*ny, :] = res[((i - 1)*ny + 1):i*ny, :] * A end return res end -obsv(sys::StateSpace) = obsv(sys.A, sys.C) +obsv(sys::AbstractStateSpace, n::Int = sys.nx) = obsv(sys.A, sys.C, n) """`ctrb(A, B)` or `ctrb(sys)` @@ -143,7 +146,7 @@ function ctrb(A::AbstractMatrix, B::AbstractMatrix) end return res end -ctrb(sys::StateSpace) = ctrb(sys.A, sys.B) +ctrb(sys::AbstractStateSpace) = ctrb(sys.A, sys.B) """`P = covar(sys, W)` @@ -165,8 +168,9 @@ function covar(sys::AbstractStateSpace, W) func = iscontinuous(sys) ? lyap : dlyap Q = try func(A, B*W*B') - catch - error("No solution to the Lyapunov equation was found in covar") + catch e + @error("No solution to the Lyapunov equation was found in covar.") + rethrow(e) end P = C*Q*C' if !isdiscrete(sys) @@ -294,7 +298,7 @@ function _infnorm_two_steps_ct(sys::AbstractStateSpace, normtype::Symbol, tol=1e return (T(opnorm(sys.D)), T(0)) end - pole_vec = pole(sys) + pole_vec = poles(sys) # Check if there is a pole on the imaginary axis pidx = findfirst(on_imag_axis, pole_vec) @@ -376,7 +380,7 @@ function _infnorm_two_steps_dt(sys::AbstractStateSpace, normtype::Symbol, tol=1e return (T(opnorm(sys.D)), Tw(0)) end - pole_vec = pole(sys) + pole_vec = poles(sys) # Check if there is a pole on the unit circle pidx = findfirst(on_unit_circle, pole_vec) @@ -490,9 +494,9 @@ end """ -`sysr, G = balreal(sys::StateSpace)` +`sysr, G, T = balreal(sys::StateSpace)` -Calculates a balanced realization of the system sys, such that the observability and reachability gramians of the balanced system are equal and diagonal `G` +Calculates a balanced realization of the system sys, such that the observability and reachability gramians of the balanced system are equal and diagonal `G`. `T` is the similarity transform between the old state `x` and the new state `z` such that `Tz = x`. See also `gram`, `baltrunc` @@ -528,15 +532,17 @@ function balreal(sys::ST) where ST <: AbstractStateSpace display(Σ) end - sysr = ST(T*sys.A/T, T*sys.B, sys.C/T, sys.D, sys.timeevol), diagm(0 => Σ) + sysr = ST(T*sys.A/T, T*sys.B, sys.C/T, sys.D, sys.timeevol), diagm(Σ), T end """ - sysr, G = baltrunc(sys::StateSpace; atol = √ϵ, rtol=1e-3, unitgain=true, n = nothing) + sysr, G, T = baltrunc(sys::StateSpace; atol = √ϵ, rtol=1e-3, unitgain=true, n = nothing) Reduces the state dimension by calculating a balanced realization of the system sys, such that the observability and reachability gramians of the balanced system are equal and diagonal `G`, and truncating it to order `n`. If `n` is not provided, it's chosen such that all states corresponding to singular values less than `atol` and less that `rtol σmax` are removed. +`T` is the similarity transform between the old state `x` and the newstate `z` such that `Tz = x`. + If `unitgain=true`, the matrix `D` is chosen such that unit static gain is achieved. See also `gram`, `balreal` @@ -544,7 +550,7 @@ See also `gram`, `balreal` Glad, Ljung, Reglerteori: Flervariabla och Olinjära metoder """ function baltrunc(sys::ST; atol = sqrt(eps()), rtol = 1e-3, unitgain = true, n = nothing) where ST <: AbstractStateSpace - sysbal, S = balreal(sys) + sysbal, S, T = balreal(sys) S = diag(S) if n === nothing S = S[S .>= atol] @@ -561,7 +567,7 @@ function baltrunc(sys::ST; atol = sqrt(eps()), rtol = 1e-3, unitgain = true, n = D = D/(C*inv(-A)*B) end - return ST(A,B,C,D,sys.timeevol), diagm(0 => S) + return ST(A,B,C,D,sys.timeevol), diagm(S), T end """ @@ -596,7 +602,7 @@ Such that `Ã` is diagonal. Returns a new scaled state-space object and the associated transformation matrix. """ -function prescale(sys::StateSpace) +function prescale(sys::AbstractStateSpace) d, S = eigen(sys.A) A = Diagonal(d) B = S\sys.B @@ -606,8 +612,8 @@ function prescale(sys::StateSpace) end """ -sysi = innovation_form(sys, R1, R2) -sysi = innovation_form(sys; sysw=I, syse=I, R1=I, R2=I) + sysi = innovation_form(sys, R1, R2) + sysi = innovation_form(sys; sysw=I, syse=I, R1=I, R2=I) Takes a system ``` @@ -634,3 +640,44 @@ function innovation_form(sys::ST; sysw=I, syse=I, R1=I, R2=I) where ST <: Abstra K = kalman(sys, covar(sysw,R1), covar(syse, R2)) ST(sys.A, K, sys.C, Matrix{eltype(sys.A)}(I, sys.ny, sys.ny), sys.timeevol) end + +""" + observer_predictor(sys::AbstractStateSpace, R1, R2) + observer_predictor(sys::AbstractStateSpace, K) + +Return the observer_predictor system +x̂' = (A - KC)x̂ + (B-KD)u + Ky +ŷ = Cx + Du +with the input equation [B K] * [u; y] + +If covariance matrices `R1, R2` are given, the kalman gain `K` is calculaded. + +See also `innovation_form`. +""" +function observer_predictor(sys::ST, R1, R2) where ST <: AbstractStateSpace + K = kalman(sys, R1, R2) + observer_predictor(sys, K) +end + +function observer_predictor(sys, K::AbstractMatrix) + A,B,C,D = ssdata(sys) + ss(A-K*C, [B-K*D K], C, [D zeros(size(D,1), size(K, 2))], sys.timeevol) +end + +""" + cont = observer_controller(sys, L::AbstractMatrix, K::AbstractMatrix) + +Return the observer_controller `cont` that is given by +`ss(A - B*L - K*C + K*D*L, K, L, 0)` + +Such that `feedback(sys, cont)` produces a closed-loop system with eigenvalues given by `A-KC` and `A-BL`. + +# Arguments: +- `sys`: Model of system +- `L`: State-feedback gain `u = -Lx` +- `K`: Observer gain +""" +function observer_controller(sys, L::AbstractMatrix, K::AbstractMatrix) + A,B,C,D = ssdata(sys) + ss(A - B*L - K*C + K*D*L, K, L, 0, sys.timeevol) +end diff --git a/src/pid_design.jl b/src/pid_design.jl index 78cd52d08..708476104 100644 --- a/src/pid_design.jl +++ b/src/pid_design.jl @@ -1,4 +1,4 @@ -export pid, pidplots, rlocus, leadlink, laglink, leadlinkat, leadlinkcurve, stabregionPID, loopshapingPI +export pid, pidplots, rlocus, leadlink, laglink, leadlinkat, leadlinkcurve, stabregionPID, loopshapingPI, placePI """ C = pid(; kp=0, ki=0; kd=0, time=false, series=false) @@ -87,7 +87,7 @@ end function getpoles(G, K) # If OrdinaryDiffEq is installed, we override getpoles with an adaptive method P = numpoly(G)[1] Q = denpoly(G)[1] - f = (y,_,k) -> ComplexF64.(Polynomials.roots(k[1]*P+Q)) + f = (y,_,k) -> sort(ComplexF64.(Polynomials.roots(k[1]*P+Q)), by=imag) prob = OrdinaryDiffEq.ODEProblem(f,f(0.,0.,0.),(0.,K[end])) integrator = OrdinaryDiffEq.init(prob,OrdinaryDiffEq.Tsit5(),reltol=1e-8,abstol=1e-8) ts = Vector{Float64}() @@ -112,15 +112,16 @@ If `OrdinaryDiffEq.jl` is installed and loaded by the user (`using OrdinaryDiffE select values of `K`. A scalar `Kmax` can then be given as second argument. """ rlocus -@recipe function rlocus(p::Rlocusplot; K=Float64[]) +@recipe function rlocus(p::Rlocusplot; K=500) P = p.args[1] - K = isempty(K) ? range(1e-6,stop=500,length=10000) : K - Z = tzero(P) + K = K isa Number ? range(1e-6,stop=K,length=10000) : K + Z = tzeros(P) poles, K = getpoles(P,K) redata = real.(poles) imdata = imag.(poles) ylim = (max(-50,minimum(imdata)), min(50,maximum(imdata))) xlim = (max(-50,minimum(redata)), min(50,maximum(redata))) + framestyle --> :zerolines title --> "Root locus" xguide --> "Re(roots)" yguide --> "Im(roots)" @@ -159,7 +160,7 @@ function laglink(a, M; h=nothing, Ts=0) Base.depwarn("`laglink($a, $M; h=$h)` is deprecated, use `laglink($a, $M; Ts=$h)` instead.", Core.Typeof(laglink).name.mt.name) Ts = h end - @assert Ts ≥ 0 "Negative `Ts` is not supported." + Ts ≥ 0 || throw(ArgumentError("Negative `Ts` is not supported.")) numerator = [1/a, 1] denominator = [M/a, 1] gain = M @@ -184,7 +185,7 @@ function leadlink(b, N, K; h=nothing, Ts=0) Base.depwarn("`leadlink($b, $N, $K; h=$h)` is deprecated, use `leadlink($b, $N, $K; Ts=$h)` instead.", Core.Typeof(leadlink).name.mt.name) Ts = h end - @assert Ts ≥ 0 "Negative `Ts` is not supported." + Ts ≥ 0 || throw(ArgumentError("Negative `Ts` is not supported.")) numerator = [1/b, 1] denominator = [1/(b*N), 1] gain = K @@ -302,3 +303,53 @@ function loopshapingPI(P,ωp; ϕl=0,rl=0, phasemargin = 0, doplot = false) end return kp,ki,C end + +""" + piparams, C = placePI(P, ω₀, ζ; form=:standard) + +Selects the parameters of a PI-controller such that the poles of +closed loop between `P` and `C` are placed to match the poles of +`s^2 + 2ζω₀ + ω₀^2`. + +The `form` keyword allows you to choose which form the PI parameters +should be returned on. +* `:standard` - `Kp*(1 + 1/Ti/s)` +* `:series` - `Kc*(1 + 1/τi/s)` +* `:parallel` - `Kp + Ki/s` +* `:Ti` - `Kp + 1/(s*Ti)` (non-standard form sometimes used in industry) + +`piparams` is a named tuple with the controller parameters. + +`C` is the transfer function of the controller. + +""" +function placePI(P, ω₀, ζ; form=:standard) + P = tf(P) + num = numvec(P)[] + den = denvec(P)[] + if length(den) != 2 || length(num) > 2 + error("Can only place poles using PI for proper first-order systems") + end + if length(num) == 1 + num = [0; num] + end + a, b = num + c, d = den + # Calculates PI on standard/series form + tmp = (a*c*ω₀^2 - 2*b*c*ζ*ω₀ + b*d) + Kp = -tmp / (a^2*ω₀^2 - 2*a*b*ω₀*ζ + b^2) + Ti = tmp / (ω₀^2*(a*d - b*c)) + C = pid(;kp=Kp, ki=Ti, time=true, series=true) + + if form === :standard + return (;Kp, Ti), C + elseif form === :series + return (;Kc=Kp, τi=Ti), C + elseif form === :parallel + return (;Kp=Kp, ki=Kp/Ti), C + elseif form === :Ti + return (;Kp=Kp, Ti=Ti/Kp), C + else + error("Form $(form) not supported") + end +end diff --git a/src/plotting.jl b/src/plotting.jl index 29d3b69fe..f9129900a 100644 --- a/src/plotting.jl +++ b/src/plotting.jl @@ -113,119 +113,37 @@ function getLogTicks(x, minmax) end -@userplot Lsimplot - -""" - fig = lsimplot(sys::LTISystem, u, t; x0=0, method) - lsimplot(LTISystem[sys1, sys2...], u, t; x0, method) - -Calculate the time response of the `LTISystem`(s) to input `u`. If `x0` is -not specified, a zero vector is used. - -Continuous time systems are discretized before simulation. By default, the -method is chosen based on the smoothness of the input signal. Optionally, the -`method` parameter can be specified as either `:zoh` or `:foh`. -""" -lsimplot - -@recipe function lsimplot(p::Lsimplot; method=nothing) - if length(p.args) < 3 - error("Wrong number of arguments") - end - systems,u,t = p.args[1:3] - - if !isa(systems,AbstractArray) - systems = [systems] - end - if method == nothing - method = _issmooth(u) ? :foh : :zoh - end - if !_same_io_dims(systems...) - error("All systems must have the same input/output dimensions") - end - ny, nu = size(systems[1]) - layout --> (ny,1) - s2i(i,j) = LinearIndices((ny,1))[j,i] - for (si,s) in enumerate(systems) - s = systems[si] - y = length(p.args) >= 4 ? lsim(s, u, t, x0=p.args[4], method=method)[1] : lsim(s, u, t, method=method)[1] - seriestype := iscontinuous(s) ? :path : :steppost +# This will be called on plot(lsim(sys, args...)) +@recipe function simresultplot(r::SimResult; plotu=false) + ny, nu = r.ny, r.nu + t = r.t + n_series = size(r.y, 3) # step and impulse produce multiple results + layout --> ((plotu ? ny + nu : ny), 1) + seriestype := iscontinuous(r.sys) ? :path : :steppost + for ms in 1:n_series for i=1:ny - ytext = (ny > 1) ? "Amplitude to: y($i)" : "Amplitude" + ytext = (ny > 1) ? "y($i)" : "y" @series begin xguide --> "Time (s)" yguide --> ytext - title --> "System Response" - subplot --> s2i(1,i) - label --> "\$G_{$(si)}\$" - t, y[:, i] + label --> (n_series > 1 ? "From u($(ms))" : "") + subplot --> i + t, r.y[i, :, ms] end end - end -end - -@userplot Stepplot -@userplot Impulseplot -""" - stepplot(sys[, tfinal]; kwargs...) or stepplot(sys[, t]; kwargs...) - -Plot step response of `sys` until final time `tfinal` or at time points in the vector `t`. -If not defined, suitable values are chosen based on `sys`. -See also [`step`](@ref) - -`kwargs` is sent as argument to Plots.plot. -""" -stepplot - -""" - impulseplot(sys[, tfinal]; kwargs...) or impulseplot(sys[, t]; kwargs...) - -Plot impulse response of `sys` until final time `tfinal` or at time points in the vector `t`. -If not defined, suitable values are chosen based on `sys`. -See also [`impulse`](@ref) - -`kwargs` is sent as argument to Plots.plot. -""" -impulseplot - -for (func, title, typ) = ((step, "Step Response", Stepplot), (impulse, "Impulse Response", Impulseplot)) - funcname = Symbol(func,"plot") - - @recipe function f(p::typ) - systems = p.args[1] - if !isa(systems, AbstractArray) - systems = [systems] - end - if !_same_io_dims(systems...) - error("All systems must have the same input/output dimensions") - end - ny, nu = size(systems[1]) - layout --> (ny,nu) - titles = fill("", 1, ny*nu) - title --> titles - s2i(i,j) = LinearIndices((ny,nu))[i,j] - for (si,s) in enumerate(systems) - y,t = func(s, p.args[2:end]...) - for i=1:ny - for j=1:nu - ydata = reshape(y[:, i, j], size(t, 1)) - style = iscontinuous(s) ? :path : :steppost - ttext = (nu > 1 && i==1) ? title*" from: u($j) " : title - titles[s2i(i,j)] = ttext - ytext = (ny > 1 && j==1) ? "Amplitude to: y($i)" : "Amplitude" - @series begin - seriestype --> style - xguide --> "Time (s)" - yguide --> ytext - subplot --> s2i(i,j) - label --> "\$G_{$(si)}\$" - t, ydata - end - end + end + if plotu # bug in recipe system, can't use `plotu || return` + for i=1:nu + utext = (nu > 1) ? "u($i)" : "u" + @series begin + xguide --> "Time (s)" + yguide --> utext + subplot --> ny+i + label --> "" + t, r.u[i, :] end end end - end """ @@ -275,6 +193,7 @@ bodeplot layout --> ((plotphase ? 2 : 1)*ny,nu) nw = length(w) xticks --> getLogTicks(ws, getlims(:xlims, plotattributes, ws)) + grid --> true for (si,s) = enumerate(systems) mag, phase = bode(s, w)[1:2] @@ -295,7 +214,6 @@ bodeplot end phasedata = vec(phase[:, i, j]) @series begin - grid --> true yscale --> _PlotScaleFunc xscale --> :log10 if _PlotScale != "dB" @@ -312,7 +230,6 @@ bodeplot plotphase || continue @series begin - grid --> true xscale --> :log10 ylims := ylimsphase yticks --> getPhaseTicks(phasedata, getlims(:ylims, plotattributes, phasedata)) @@ -371,63 +288,81 @@ end @userplot Nyquistplot """ - fig = nyquistplot(sys; gaincircles=true, kwargs...) - nyquistplot(LTISystem[sys1, sys2...]; gaincircles=true, kwargs...) + fig = nyquistplot(sys; Ms_circles=Float64[], unit_circle=false, hz = false, kwargs...) + nyquistplot(LTISystem[sys1, sys2...]; Ms_circles=Float64[], unit_circle=false, kwargs...) Create a Nyquist plot of the `LTISystem`(s). A frequency vector `w` can be optionally provided. -`gaincircles` plots the circles corresponding to |S(iω)| = 1 and |T(iω)| = 1, where S and T are -the sensitivity and complementary sensitivity functions. +- `unit_circle`: if the unit circle should be displayed +- `Ms_circles`: draw circles corresponding to given levels of sensitivity (circles around -1 with radii `1/Ms`). `Ms_circles` can be supplied as a number or a vector of numbers. A design staying outside such a circle has a phase margin of at least `2asin(1/(2Ms))` rad and a gain margin of at least `Ms/(Ms-1)`. + +If `hz=true`, the hover information will be displayed in Hertz, the input frequency vector is still treated as rad/s. `kwargs` is sent as argument to plot. """ nyquistplot -@recipe function nyquistplot(p::Nyquistplot; gaincircles=true) +@recipe function nyquistplot(p::Nyquistplot; Ms_circles=Float64[], unit_circle=false, hz=false) systems, w = _processfreqplot(Val{:nyquist}(), p.args...) ny, nu = size(systems[1]) nw = length(w) layout --> (ny,nu) - s2i(i,j) = LinearIndices((ny,nu))[j,i] - # Ensure that `axes` is always a matrix of handles + framestyle --> :zerolines + s2i(i,j) = LinearIndices((nu,ny))[j,i] + θ = range(0, stop=2π, length=100) + S, C = sin.(θ), cos.(θ) for (si,s) = enumerate(systems) re_resp, im_resp = nyquist(s, w)[1:2] for j=1:nu for i=1:ny - redata = re_resp[:, i, j] - imdata = im_resp[:, i, j] + redata = re_resp[:, i, j] + imdata = im_resp[:, i, j] @series begin - ylims --> (min(max(-20,minimum(imdata)),-1), max(min(20,maximum(imdata)),1)) - xlims --> (min(max(-20,minimum(redata)),-1), max(min(20,maximum(redata)),1)) + ylims --> (min(max(-20,minimum(imdata)),-1), max(min(20,maximum(imdata)),1)) + xlims --> (min(max(-20,minimum(redata)),-1), max(min(20,maximum(redata)),1)) title --> "Nyquist plot from: u($j)" yguide --> "To: y($i)" subplot --> s2i(i,j) label --> "\$G_{$(si)}\$" - hover --> [Printf.@sprintf("ω = %.3f", w) for w in w] + hover --> [hz ? Printf.@sprintf("f = %.3f", w/2π) : Printf.@sprintf("ω = %.3f", w) for w in w] (redata, imdata) - end - # Plot rings - if gaincircles && si == length(systems) - v = range(0,stop=2π,length=100) - S,C = sin.(v),cos.(v) - @series begin + end + + if si == length(systems) + @series begin # Mark the critical point + subplot --> s2i(i,j) primary := false - linestyle := :dash - linecolor := :black - seriestype := :path - markershape := :none - (C,S) + markershape := :xcross + seriescolor := :red + markersize := 5 + seriesstyle := :scatter + [-1], [0] end - @series begin - primary := false - linestyle := :dash - linecolor := :black - seriestype := :path - markershape := :none - (C .-1,S) + for Ms in Ms_circles + @series begin + subplot --> s2i(i,j) + primary := false + linestyle := :dash + linecolor := :gray + seriestype := :path + markershape := :none + label := "Ms = $(round(Ms, digits=2))" + (-1 .+ (1/Ms) * C, (1/Ms) * S) + end + end + if unit_circle + @series begin + subplot --> s2i(i,j) + primary := false + linestyle := :dash + linecolor := :gray + seriestype := :path + markershape := :none + (C, S) + end end end - + end end end @@ -603,21 +538,24 @@ end @userplot Sigmaplot """ - sigmaplot(sys, args...) - sigmaplot(LTISystem[sys1, sys2...], args...) + sigmaplot(sys, args...; hz=false) + sigmaplot(LTISystem[sys1, sys2...], args...; hz=false) Plot the singular values of the frequency response of the `LTISystem`(s). A frequency vector `w` can be optionally provided. +If `hz=true`, the plot x-axis will be displayed in Hertz, the input frequency vector is still treated as rad/s. + `kwargs` is sent as argument to Plots.plot. """ sigmaplot -@recipe function sigmaplot(p::Sigmaplot) +@recipe function sigmaplot(p::Sigmaplot; hz=false) systems, w = _processfreqplot(Val{:sigma}(), p.args...) + ws = (hz ? 1/(2π) : 1) .* w ny, nu = size(systems[1]) nw = length(w) title --> "Sigma Plot" - xguide --> "Frequency (rad/s)", + xguide --> (hz ? "Frequency [Hz]" : "Frequency [rad/s]") yguide --> "Singular Values $_PlotScaleStr" for (si, s) in enumerate(systems) sv = sigma(s, w)[1] @@ -629,7 +567,7 @@ sigmaplot xscale --> :log10 yscale --> _PlotScaleFunc seriescolor --> si - w, sv[:, i] + ws, sv[:, i] end end end @@ -731,34 +669,35 @@ Create a pole-zero map of the `LTISystem`(s) in figure `fig`, `args` and `kwargs pzmap @recipe function pzmap(p::Pzmap) systems = p.args[1] - if systems[1].nu + systems[1].ny > 2 - @warn("pzmap currently only supports SISO systems. Only transfer function from u₁ to y₁ will be shown") - end seriestype := :scatter framestyle --> :zerolines title --> "Pole-zero map" legend --> false - for system in systems - z,p,k = zpkdata(system) - if !isempty(z[1]) + for (i, system) in enumerate(systems) + p = poles(system) + z = tzeros(system) + if !isempty(z) @series begin + group --> i markershape --> :c markersize --> 15. markeralpha --> 0.5 - real(z[1]),imag(z[1]) + real(z),imag(z) end end - if !isempty(p[1]) + if !isempty(p) @series begin - markershape --> :x + group --> i + markershape --> :xcross markersize --> 15. - real(p[1]),imag(p[1]) + markeralpha --> 0.5 + real(p),imag(p) end end if isdiscrete(system) - v = range(0,stop=2π,length=100) - S,C = sin.(v),cos.(v) + θ = range(0,stop=2π,length=100) + S,C = sin.(θ),cos.(θ) @series begin linestyle --> :dash linecolor := :black @@ -777,8 +716,7 @@ pzmap!(sys::LTISystem; kwargs...) = pzmap!([sys]; kwargs...) Gang-of-Four plot. `kwargs` is sent as argument to Plots.plot. """ -function gangoffourplot(P::Union{Vector, LTISystem}, C::Vector, args...; minimal=true, plotphase=false, kwargs...) - # Array of (S,D,N,T) +function gangoffourplot(P::Union{Vector, LTISystem}, C::Vector, args...; minimal=true, plotphase=false, kwargs...) if P isa LTISystem # Don't broadcast over scalar (with size?) P = [P] end @@ -788,7 +726,7 @@ function gangoffourplot(P::Union{Vector, LTISystem}, C::Vector, args...; minimal titles = fill("", 1, plotphase ? 8 : 4) # Empty titles on phase titleIdx = plotphase ? [1,2,5,6] : [1,2,3,4] - titles[titleIdx] = ["S = 1/(1+PC)", "D = P/(1+PC)", "N = C/(1+PC)", "T = PC/(1+PC)"] + titles[titleIdx] = ["S = 1/(1+PC)", "P/(1+PC)", "C/(1+PC)", "T = PC/(1+PC)"] Plots.plot!(fig, title = titles) return fig end diff --git a/src/simplification.jl b/src/simplification.jl index 66fbd53ff..cd8c2bd31 100644 --- a/src/simplification.jl +++ b/src/simplification.jl @@ -4,7 +4,16 @@ Compute the structurally minimal realization of the state-space system `sys`. A structurally minimal realization is one where only states that can be determined to be uncontrollable and unobservable based on the location of 0s in -`sys` are removed.""" +`sys` are removed. + +Systems with numerical noise in the coefficients, e.g., noise on the order of `eps` require truncation to zero to be +affected by structural simplification, e.g., +```julia +trunc_zero!(A) = A[abs.(A) .< 10eps(maximum(abs, A))] .= 0 +trunc_zero!(sys.A); trunc_zero!(sys.B); trunc_zero!(sys.C) +sminreal(sys) +``` +""" function sminreal(sys::StateSpace) A, B, C, inds = struct_ctrb_obsv(sys) return StateSpace(A, B, C, sys.D, sys.timeevol) @@ -12,6 +21,7 @@ end # Determine the structurally controllable and observable realization for the system struct_ctrb_obsv(sys::StateSpace) = struct_ctrb_obsv(sys.A, sys.B, sys.C) + function struct_ctrb_obsv(A::AbstractVecOrMat, B::AbstractVecOrMat, C::AbstractVecOrMat) costates = struct_ctrb_states(A, B) .& struct_ctrb_states(A', C') if !all(costates) @@ -22,15 +32,17 @@ function struct_ctrb_obsv(A::AbstractVecOrMat, B::AbstractVecOrMat, C::AbstractV end end -# Compute a bit-vector, expressing whether a state of the pair (A, B) is -# structurally controllable. +""" + struct_ctrb_states(A::AbstractVecOrMat, B::AbstractVecOrMat) + +Compute a bit-vector, expressing whether a state of the pair (A, B) is +structurally controllable based on the location of zeros in the matrices. +""" function struct_ctrb_states(A::AbstractVecOrMat, B::AbstractVecOrMat) - bitA = A .!= 0 - d_cvec = cvec = vec(any(B .!= 0, dims=2)) - while any(d_cvec .!= 0) - Adcvec = vec(any(bitA[:, findall(d_cvec)], dims=2)) - cvec = cvec .| Adcvec - d_cvec = Adcvec .& .~cvec + bitA = .!iszero.(A) + x = vec(any(B .!= 0, dims=2)) # index vector indicating states that have been affected by input + for i = 1:size(A, 1) # apply A nx times, similar to controllability matrix + x = x .| .!iszero.(bitA * x) end - return cvec + x end diff --git a/src/simulators.jl b/src/simulators.jl index 2895e7f4d..069c2fd0d 100644 --- a/src/simulators.jl +++ b/src/simulators.jl @@ -37,7 +37,7 @@ plot(t, s.y(sol, t)[:], lab="Open loop step response") ``` """ function Simulator(P::AbstractStateSpace, u::F = (x,t) -> 0) where F - @assert iscontinuous(P) "Simulator only supports continuous-time system. See function `lsim` for simulation of discrete-time systems." + iscontinuous(P) || throw(ArgumentError("Simulator only supports continuous-time system. See function `lsim` for simulation of discrete-time systems.")) f = (dx,x,p,t) -> dx .= P.A*x .+ P.B*u(x,t) y(x,t) = P.C*x .+ P.D*u(x,t) y(sol::ODESolution,t) = P.C*sol(t) .+ P.D*u(sol(t),t) diff --git a/src/synthesis.jl b/src/synthesis.jl index ac597bea7..ca3c3ddd7 100644 --- a/src/synthesis.jl +++ b/src/synthesis.jl @@ -13,8 +13,6 @@ For the continuous time model `dx = Ax + Bu`. Solve the LQR problem for state-space system `sys`. Works for both discrete and continuous time systems. -See also `LQG` - Usage example: ```julia using LinearAlgebra # For identity matrix I @@ -30,7 +28,7 @@ u(x,t) = -L*x # Form control law, t=0:0.1:5 x0 = [1,0] y, t, x, uout = lsim(sys,u,t,x0=x0) -plot(t,x, lab=["Position" "Velocity"], xlabel="Time [s]") +plot(t,x', lab=["Position" "Velocity"], xlabel="Time [s]") ``` """ function lqr(A, B, Q, R) @@ -44,12 +42,10 @@ end kalman(sys, R1, R2) Calculate the optimal Kalman gain - -See also `LQG` """ kalman(A, C, R1,R2) = Matrix(lqr(A',C',R1,R2)') -function lqr(sys::StateSpace, Q, R) +function lqr(sys::AbstractStateSpace, Q, R) if iscontinuous(sys) return lqr(sys.A, sys.B, Q, R) else @@ -57,7 +53,7 @@ function lqr(sys::StateSpace, Q, R) end end -function kalman(sys::StateSpace, R1,R2) +function kalman(sys::AbstractStateSpace, R1,R2) if iscontinuous(sys) return Matrix(lqr(sys.A', sys.C', R1,R2)') else @@ -95,7 +91,7 @@ u(x,t) = -L*x # Form control law, t=0:Ts:5 x0 = [1,0] y, t, x, uout = lsim(sys,u,t,x0=x0) -plot(t,x, lab=["Position" "Velocity"], xlabel="Time [s]") +plot(t,x', lab=["Position" "Velocity"], xlabel="Time [s]") ``` """ function dlqr(A, B, Q, R) @@ -104,7 +100,7 @@ function dlqr(A, B, Q, R) return K end -function dlqr(sys::StateSpace, Q, R) +function dlqr(sys::AbstractStateSpace, Q, R) !isdiscrete(sys) && throw(ArgumentError("Input argument sys must be discrete-time system")) return dlqr(sys.A, sys.B, Q, R) end @@ -119,45 +115,52 @@ Calculate the optimal Kalman gain for discrete time systems dkalman(A, C, R1,R2) = Matrix(dlqr(A',C',R1,R2)') """ - place(A, B, p) - place(sys::StateSpace, p) + place(A, B, p, opt=:c) + place(sys::StateSpace, p, opt=:c) + +Calculate the gain matrix `K` such that `A - BK` has eigenvalues `p`. -Calculate gain matrix `K` such that -the poles of `(A-BK)` in are in `p`. + place(A, C, p, opt=:o) + place(sys::StateSpace, p, opt=:o) + +Calculate the observer gain matrix `L` such that `A - LC` has eigenvalues `p`. Uses Ackermann's formula. -For observer pole placement, see `luenberger`. +Currently handles only SISO systems. """ -function place(A, B, p) +function place(A, B, p, opt=:c) n = length(p) - n != size(A,1) && error("Must define as many poles as states") - n != size(B,1) && error("A and B must have same number of rows") - if size(B,2) == 1 - acker(A,B,p) + n != size(A,1) && error("Must specify as many poles as states") + if opt === :c + n != size(B,1) && error("A and B must have same number of rows") + if size(B,2) == 1 + acker(A, B, p) + else + error("place only implemented for SISO systems") + end + elseif opt === :o + C = B # B is really the "C matrix" + n != size(C,2) && error("A and C must have same number of columns") + if size(C,1) == 1 + acker(A', C', p)' + else + error("place only implemented for SISO systems") + end else - error("place only implemented for SISO systems") + error("fourth argument must be :c or :o") end end - -function place(sys::StateSpace, p) - return place(sys.A, sys.B, p) +function place(sys::AbstractStateSpace, p, opt=:c) + if opt === :c + return place(sys.A, sys.B, p, opt) + elseif opt === :o + return place(sys.A, sys.C, p, opt) + else + error("third argument must be :c or :o") + end end -""" - luenberger(A, C, p) - luenberger(sys::StateSpace, p) - -Calculate gain matrix `L` such that the poles of `(A - LC)` are in `p`. -Uses sytem's dual form (Controllability-Observability duality) applied to Ackermann's formula. -That is, `(A - BK)` is indentic to `(A' - C'L') == (A - LC)`. -""" -function luenberger(A, C, p) - place(A', C', p)' -end -function luenberger(sys::StateSpace, p) - return luenberger(sys.A, sys.C, p) -end #Implements Ackermann's formula for placing poles of (A-BK) in p function acker(A,B,P) diff --git a/src/timeresp.jl b/src/timeresp.jl index 5bbae6a3c..b820d8ec0 100644 --- a/src/timeresp.jl +++ b/src/timeresp.jl @@ -10,23 +10,24 @@ Calculate the step response of system `sys`. If the final time `tfinal` or time vector `t` is not provided, one is calculated based on the system pole locations. -`y` has size `(length(t), ny, nu)`, `x` has size `(length(t), nx, nu)`""" -function Base.step(sys::AbstractStateSpace, t::AbstractVector; method=:cont) - lt = length(t) +`y` has size `(ny, length(t), nu)`, `x` has size `(nx, length(t), nu)`""" +function Base.step(sys::AbstractStateSpace, t::AbstractVector; method=:cont, kwargs...) + T = promote_type(eltype(sys.A), Float64) ny, nu = size(sys) - nx = sys.nx - u = (x,t)->[one(eltype(t))] - x0 = zeros(nx) + nx = nstates(sys) + u_element = [one(eltype(t))] # to avoid allocating this multiple times + u = (x,t)->u_element + x0 = zeros(T, nx) if nu == 1 - y, tout, x, _ = lsim(sys, u, t, x0=x0, method=method) + y, tout, x, uout = lsim(sys, u, t; x0, method, kwargs...) else - x = Array{Float64}(undef, lt, nx, nu) - y = Array{Float64}(undef, lt, ny, nu) + x = Array{T}(undef, nx, length(t), nu) + y = Array{T}(undef, ny, length(t), nu) for i=1:nu - y[:,:,i], tout, x[:,:,i],_ = lsim(sys[:,i], u, t, x0=x0, method=method) + y[:,:,i], tout, x[:,:,i], uout = lsim(sys[:,i], u, t; x0, method, kwargs...) end end - return y, t, x + return SimResult(y, t, x, uout, sys) end Base.step(sys::LTISystem, tfinal::Real; kwargs...) = step(sys, _default_time_vector(sys, tfinal); kwargs...) @@ -41,12 +42,11 @@ Calculate the impulse response of system `sys`. If the final time `tfinal` or ti vector `t` is not provided, one is calculated based on the system pole locations. -`y` has size `(length(t), ny, nu)`, `x` has size `(length(t), nx, nu)`""" -function impulse(sys::AbstractStateSpace, t::AbstractVector; method=:cont) +`y` has size `(ny, length(t), nu)`, `x` has size `(nx, length(t), nu)`""" +function impulse(sys::AbstractStateSpace, t::AbstractVector; kwargs...) T = promote_type(eltype(sys.A), Float64) - lt = length(t) ny, nu = size(sys) - nx = sys.nx + nx = nstates(sys) if iscontinuous(sys) #&& method === :cont u = (x,t) -> [zero(T)] # impulse response equivalent to unforced response of @@ -59,31 +59,35 @@ function impulse(sys::AbstractStateSpace, t::AbstractVector; method=:cont) x0s = zeros(T, nx, nu) end if nu == 1 # Why two cases # QUESTION: Not type stable? - y, t, x,_ = lsim(sys, u, t, x0=x0s[:], method=method) + y, t, x, uout = lsim(sys, u, t; x0=x0s[:], kwargs...) else - x = Array{T}(undef, lt, nx, nu) - y = Array{T}(undef, lt, ny, nu) + x = Array{T}(undef, nx, length(t), nu) + y = Array{T}(undef, ny, length(t), nu) for i=1:nu - y[:,:,i], t, x[:,:,i],_ = lsim(sys[:,i], u, t, x0=x0s[:,i], method=method) + y[:,:,i], t, x[:,:,i], uout = lsim(sys[:,i], u, t; x0=x0s[:,i], kwargs...) end end - return y, t, x + return SimResult(y, t, x, uout, sys) end -impulse(sys::LTISystem, tfinal::Real; kwags...) = impulse(sys, _default_time_vector(sys, tfinal); kwags...) -impulse(sys::LTISystem; kwags...) = impulse(sys, _default_time_vector(sys); kwags...) -impulse(sys::TransferFunction, t::AbstractVector; kwags...) = impulse(ss(sys), t; kwags...) +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...) """ - y, t, x = lsim(sys, u[, t]; x0, method]) - y, t, x, uout = lsim(sys, u::Function, t; x0, method) + result = lsim(sys, u[, t]; x0, method]) + result = lsim(sys, u::Function, t; x0, method) Calculate the time response of system `sys` to input `u`. If `x0` is ommitted, a zero vector is used. -`y`, `x`, `uout` has time in the first dimension. Initial state `x0` defaults to zero. +The result structure contains the fields `y, t, x, u` and can be destructured automatically by iteration, e.g., +```julia +y, t, x, u = result +``` +`y, `x`, `u` have time in the second dimension. Initial state `x0` defaults to zero. -Continuous time systems are simulated using an ODE solver if `u` is a function. If `u` is an array, the system is discretized before simulation. For a lower level inteface, see `?Simulator` and `?solve` +Continuous time systems are simulated using an ODE solver if `u` is a function. If `u` is an array, the system is discretized (with `method=:zoh` by default) before simulation. For a lower level inteface, see `?Simulator` and `?solve` `u` can be a function or a matrix/vector of precalculated control signals. If `u` is a function, then `u(x,i)` (`u(x,t)`) is called to calculate the control signal every iteration (time instance used by solver). This can be used to provide a control law such as state feedback `u(x,t) = -L*x` calculated by `lqr`. @@ -106,19 +110,19 @@ u(x,t) = -L*x # Form control law, t=0:0.1:5 x0 = [1,0] y, t, x, uout = lsim(sys,u,t,x0=x0) -plot(t,x, lab=["Position" "Velocity"], xlabel="Time [s]") +plot(t,x', lab=["Position" "Velocity"], xlabel="Time [s]") ``` """ function lsim(sys::AbstractStateSpace, u::AbstractVecOrMat, t::AbstractVector; - x0::AbstractVector=zeros(Bool, sys.nx), method::Symbol=:unspecified) + x0::AbstractVecOrMat=zeros(Bool, nstates(sys)), method::Symbol=:zoh) ny, nu = size(sys) nx = sys.nx if length(x0) != nx error("size(x0) must match the number of states of sys") end - if !(size(u) in [(length(t), nu) (length(t),)]) - error("u must be of size (length(t), nu)") + if size(u) != (nu, length(t)) + error("u must be of size (nu, length(t))") end dt = Float64(t[2] - t[1]) @@ -127,17 +131,13 @@ function lsim(sys::AbstractStateSpace, u::AbstractVecOrMat, t::AbstractVector; end if iscontinuous(sys) - if method === :unspecified - method = _issmooth(u) ? :foh : :zoh - end - if method === :zoh dsys = c2d(sys, dt, :zoh) elseif method === :foh dsys, x0map = c2d_x0map(sys, dt, :foh) - x0 = x0map*[x0; transpose(u)[:,1]] + x0 = x0map*[x0; u[:,1]] else - error("Unsupported discretization method") + error("Unsupported discretization method: $method") end else if sys.Ts != dt @@ -147,25 +147,31 @@ function lsim(sys::AbstractStateSpace, u::AbstractVecOrMat, t::AbstractVector; end x = ltitr(dsys.A, dsys.B, u, x0) - y = transpose(sys.C*transpose(x) + sys.D*transpose(u)) - return y, t, x + y = sys.C*x + sys.D*u + return SimResult(y, t, x, u, dsys) # saves the system that actually produced the simulation end function lsim(sys::AbstractStateSpace{<:Discrete}, u::AbstractVecOrMat; kwargs...) - t = range(0, length=size(u, 1), step=sys.Ts) + t = range(0, length=size(u, 2), step=sys.Ts) lsim(sys, u, t; kwargs...) end -@deprecate lsim(sys, u, t, x0) lsim(sys, u, t; x0=x0) -@deprecate lsim(sys, u, t, x0, method) lsim(sys, u, t; x0=x0, method=method) +@deprecate lsim(sys, u, t, x0) lsim(sys, u, t; x0) +@deprecate lsim(sys, u, t, x0, method) lsim(sys, u, t; x0, method) -function lsim(sys::AbstractStateSpace, u::Function, tfinal::Real, args...; kwargs...) +function lsim(sys::AbstractStateSpace, u::Function, tfinal::Real; kwargs...) t = _default_time_vector(sys, tfinal) - lsim(sys, u, t, args...; kwargs...) + lsim(sys, u, t; kwargs...) +end + +# Function for DifferentialEquations lsim +function f_lsim(dx, x, p, t) + A, B, u = p + dx .= A * x .+ B * u(x, t) end function lsim(sys::AbstractStateSpace, u::Function, t::AbstractVector; - x0::VecOrMat=zeros(sys.nx), method::Symbol=:cont) + x0::AbstractVecOrMat=zeros(Bool, nstates(sys)), method::Symbol=:cont, alg = Tsit5(), kwargs...) ny, nu = size(sys) nx = sys.nx u0 = u(x0,1) @@ -180,29 +186,27 @@ function lsim(sys::AbstractStateSpace, u::Function, t::AbstractVector; if !iscontinuous(sys) || method === :zoh if iscontinuous(sys) - dsys = c2d(sys, dt, :zoh) + simsys = c2d(sys, dt, :zoh) else if sys.Ts != dt error("Time vector must match sample time for discrete system") end - dsys = sys + simsys = sys end - x,uout = ltitr(dsys.A, dsys.B, u, t, T.(x0)) + x,uout = ltitr(simsys.A, simsys.B, u, t, T.(x0)) else - s = Simulator(sys, u) - sol = solve(s, T.(x0), (t[1],t[end]), Tsit5()) - x = sol(t)' - uout = Array{eltype(x)}(undef, length(t), ninputs(sys)) - for (i,ti) in enumerate(t) - uout[i,:] = u(x[i,:],ti)' - end + p = (sys.A, sys.B, u) + sol = solve(ODEProblem(f_lsim, x0, (t[1], t[end]), p), alg; saveat=t, kwargs...) + x = reduce(hcat, sol.u) + uout = reduce(hcat, u(x[:, i], t[i]) for i in eachindex(t)) + simsys = sys end - y = transpose(sys.C*transpose(x) + sys.D*transpose(uout)) - return y, t, x, uout + y = sys.C*x + sys.D*uout + return SimResult(y, t, x, uout, simsys) # saves the system that actually produced the simulation end -lsim(sys::TransferFunction, u, t, args...; kwargs...) = lsim(ss(sys), u, t, args...; kwargs...) +lsim(sys::TransferFunction, u, t; kwargs...) = lsim(ss(sys), u, t; kwargs...) """ @@ -218,15 +222,12 @@ e.g, `x0` should prefereably not be a sparse vector. If `u` is a function, then `u(x,i)` is called to calculate the control signal every iteration. This can be used to provide a control law such as state feedback `u=-Lx` calculated by `lqr`. In this case, an integrer `iters` must be provided that indicates the number of iterations. """ @views function ltitr(A::AbstractMatrix, B::AbstractMatrix, u::AbstractVecOrMat, - x0::AbstractVector=zeros(eltype(A), size(A, 1))) + x0::AbstractVecOrMat=zeros(eltype(A), size(A, 1))) T = promote_type(LinearAlgebra.promote_op(LinearAlgebra.matprod, eltype(A), eltype(x0)), LinearAlgebra.promote_op(LinearAlgebra.matprod, eltype(B), eltype(u))) - n = size(u, 1) - - # Transposing u allows column-wise operations, which apparently is faster. - ut = transpose(u) + n = size(u, 2) # Using similar instead of Matrix{T} to allow for CuArrays to be used. # This approach is problematic if x0 is sparse for example, but was considered @@ -234,18 +235,16 @@ If `u` is a function, then `u(x,i)` is called to calculate the control signal ev x = similar(x0, T, (length(x0), n)) x[:,1] .= x0 - mul!(x[:, 2:end], B, transpose(u[1:end-1, :])) # Do all multiplications B*u[:,k] to save view allocations + mul!(x[:, 2:end], B, u[:, 1:end-1]) # Do all multiplications B*u[:,k] to save view allocations - tmp = similar(x0, T) # temporary vector for storing A*x[:,k] for k=1:n-1 - mul!(tmp, A, x[:,k]) - x[:,k+1] .+= tmp + mul!(x[:, k+1], A, x[:,k], true, true) end - return transpose(x) + return x end function ltitr(A::AbstractMatrix{T}, B::AbstractMatrix{T}, u::Function, t, - x0::VecOrMat=zeros(T, size(A, 1))) where T + x0::AbstractVecOrMat=zeros(T, size(A, 1))) where T iters = length(t) x = similar(A, size(A, 1), iters) uout = similar(A, size(B, 2), iters) @@ -255,7 +254,7 @@ function ltitr(A::AbstractMatrix{T}, B::AbstractMatrix{T}, u::Function, t, uout[:,i] .= u(x0,t[i]) x0 = A * x0 + B * uout[:,i] end - return transpose(x), transpose(uout) + return x, uout end # HELPERS: @@ -273,24 +272,11 @@ end function _default_dt(sys::LTISystem) if isdiscrete(sys) return sys.Ts - elseif all(iszero, pole(sys)) # Static or pure integrators + elseif all(iszero, poles(sys)) # Static or pure integrators return 0.05 else - ω0_max = maximum(abs.(pole(sys))) + ω0_max = maximum(abs.(poles(sys))) dt = round(1/(12*ω0_max), sigdigits=2) return dt end end - - - -#TODO a reasonable check -_issmooth(u::Function) = false - -# Determine if a signal is "smooth" -function _issmooth(u, thresh::AbstractFloat=0.75) - u = [zeros(1, size(u, 2)); u] # Start from 0 signal always - dist = maximum(u) - minimum(u) - du = abs.(diff(u, dims=1)) - return !isempty(du) && all(maximum(du) <= thresh*dist) -end diff --git a/src/types/DelayLtiSystem.jl b/src/types/DelayLtiSystem.jl index b403c5200..14d5f7162 100644 --- a/src/types/DelayLtiSystem.jl +++ b/src/types/DelayLtiSystem.jl @@ -97,7 +97,7 @@ function +(sys::DelayLtiSystem{T}, n::T) where {T<:Number} new_D = copy(ssold.D) new_D[1:ny, 1:nu] .+= n - pnew = PartionedStateSpace(StateSpace(ssold.A, ssold.B, ssold.C, new_D, 0.0), ny, nu) + pnew = PartionedStateSpace(StateSpace(ssold.A, ssold.B, ssold.C, new_D, Continuous()), ny, nu) DelayLtiSystem(pnew, sys.Tau) end @@ -160,7 +160,7 @@ function Base.show(io::IO, sys::DelayLtiSystem) print(io, "\nP: ") show(io, sys.P.P) - println(io, "\n\nDelays: $(sys.Tau)") + print(io, "\n\nDelays: $(sys.Tau)") end diff --git a/src/types/Lti.jl b/src/types/Lti.jl index 5264c797b..9bb3653e5 100644 --- a/src/types/Lti.jl +++ b/src/types/Lti.jl @@ -85,11 +85,11 @@ common_timeevol(systems::LTISystem...) = common_timeevol(timeevol(sys) for sys i Returns `true` if `sys` is stable, else returns `false`.""" function isstable(sys::LTISystem) if iscontinuous(sys) - if any(real.(pole(sys)).>=0) + if any(real.(poles(sys)).>=0) return false end else - if any(abs.(pole(sys)).>=1) + if any(abs.(poles(sys)).>=1) return false end end diff --git a/src/types/SisoTfTypes/SisoRational.jl b/src/types/SisoTfTypes/SisoRational.jl index 8ad4905a5..c05b6f2d3 100644 --- a/src/types/SisoTfTypes/SisoRational.jl +++ b/src/types/SisoTfTypes/SisoRational.jl @@ -76,8 +76,8 @@ denvec(f::SisoRational) = reverse(coeffs(f.den)) denpoly(f::SisoRational) = f.den numpoly(f::SisoRational) = f.num -tzero(f::SisoRational) = roots(f.num) -pole(f::SisoRational) = roots(f.den) +tzeros(f::SisoRational) = roots(f.num) +poles(f::SisoRational) = roots(f.den) function evalfr(f::SisoRational{T}, s::Number) where T S = promote_op(/, promote_type(T, typeof(s)), promote_type(T, typeof(s))) diff --git a/src/types/SisoTfTypes/SisoZpk.jl b/src/types/SisoTfTypes/SisoZpk.jl index 4c493c332..1628c393b 100644 --- a/src/types/SisoTfTypes/SisoZpk.jl +++ b/src/types/SisoTfTypes/SisoZpk.jl @@ -13,8 +13,8 @@ struct SisoZpk{T,TR<:Number} <: SisoTf{T} end if TR <: Complex && T <: Real z, p = copy(z), copy(p) - @assert pairup_conjugates!(z) "zpk model should be real-valued, but zeros do not come in conjugate pairs." - @assert pairup_conjugates!(p) "zpk model should be real-valued, but poles do not come in conjugate pairs." + pairup_conjugates!(z) || throw(ArgumentError("zpk model should be real-valued, but zeros do not come in conjugate pairs.")) + pairup_conjugates!(p) || throw(ArgumentError("zpk model should be real-valued, but poles do not come in conjugate pairs.")) end new{T,TR}(z, p, k) end @@ -44,9 +44,9 @@ Base.one(f::SisoZpk) = one(typeof(f)) Base.zero(f::SisoZpk) = zero(typeof(f)) -# tzero is not meaningful for transfer function element? But both zero and zeros are taken... -tzero(f::SisoZpk) = f.z # Do minreal first?, -pole(f::SisoZpk) = f.p # Do minreal first? +# tzeros is not meaningful for transfer function element? But both zero and zeros are taken... +tzeros(f::SisoZpk) = f.z # Do minreal first?, +poles(f::SisoZpk) = f.p # Do minreal first? numpoly(f::SisoZpk{<:Real}) = f.k*prod(roots2real_poly_factors(f.z)) denpoly(f::SisoZpk{<:Real}) = prod(roots2real_poly_factors(f.p)) diff --git a/src/types/StateSpace.jl b/src/types/StateSpace.jl index 4b2d3f329..937bbeaed 100644 --- a/src/types/StateSpace.jl +++ b/src/types/StateSpace.jl @@ -50,10 +50,10 @@ function StateSpace(A::Matrix{T}, B::Matrix{T}, C::Matrix{T}, D::Matrix{T}) wher StateSpace(A, B, C, D, Continuous()) end -""" If D=0 then convert to correct size and type, else, create 1x1 matrix""" +""" If D=0 then create zero matrix of correct size and type, else, convert D to correct type""" function fix_D_matrix(T::Type,B,C,D) if D == 0 - D = fill(zero(T), size(C,1), size(B,2)) + D = zeros(T, size(C,1), size(B,2)) else D = to_matrix(T, D) end @@ -96,9 +96,9 @@ StateSpace(A::AbstractNumOrArray, B::AbstractNumOrArray, C::AbstractNumOrArray, # Function for creation of static gain function StateSpace(D::AbstractArray{T}, timeevol::TimeEvolution) where {T<:Number} ny, nu = size(D, 1), size(D, 2) - A = fill(zero(T), 0, 0) - B = fill(zero(T), 0, nu) - C = fill(zero(T), ny, 0) + A = zeros(T, 0, 0) + B = zeros(T, 0, nu) + C = zeros(T, ny, 0) D = reshape(D, (ny,nu)) return StateSpace(A, B, C, D, timeevol) end @@ -170,9 +170,9 @@ HeteroStateSpace(s::AbstractStateSpace) = HeteroStateSpace(s.A,s.B,s.C,s.D,s.tim # Function for creation of static gain function HeteroStateSpace(D::AbstractArray{T}, timeevol::TimeEvolution) where {T<:Number} ny, nu = size(D, 1), size(D, 2) - A = fill(zero(T), 0, 0) - B = fill(zero(T), 0, nu) - C = fill(zero(T), ny, 0) + A = zeros(T, 0, 0) + B = zeros(T, 0, nu) + C = zeros(T, ny, 0) return HeteroStateSpace(A, B, C, D, timeevol) end @@ -217,6 +217,8 @@ function isapprox(sys1::ST1, sys2::ST2; kwargs...) where {ST1<:AbstractStateSpac end ## ADDITION ## +Base.zero(sys::AbstractStateSpace) = ss(zero(sys.D), sys.timeevol) + function +(s1::StateSpace{TE,T}, s2::StateSpace{TE,T}) where {TE,T} #Ensure systems have same dimensions if size(s1) != size(s2) @@ -224,8 +226,8 @@ function +(s1::StateSpace{TE,T}, s2::StateSpace{TE,T}) where {TE,T} end timeevol = common_timeevol(s1,s2) - A = [s1.A fill(zero(T), nstates(s1), nstates(s2)); - fill(zero(T), nstates(s2), nstates(s1)) s2.A] + A = [s1.A zeros(T, nstates(s1), nstates(s2)); + zeros(T, nstates(s2), nstates(s1)) s2.A] B = [s1.B ; s2.B] C = [s1.C s2.C;] D = [s1.D + s2.D;] @@ -240,8 +242,8 @@ function +(s1::HeteroStateSpace, s2::HeteroStateSpace) end timeevol = common_timeevol(s1,s2) T = promote_type(eltype(s1.A),eltype(s2.A)) - A = [s1.A fill(zero(T), nstates(s1), nstates(s2)); - fill(zero(T), nstates(s2), nstates(s1)) s2.A] + A = [s1.A zeros(T, nstates(s1), nstates(s2)); + zeros(T, nstates(s2), nstates(s1)) s2.A] B = [s1.B ; s2.B] C = [s1.C s2.C;] D = [s1.D + s2.D;] @@ -270,7 +272,7 @@ function *(sys1::StateSpace{TE,T}, sys2::StateSpace{TE,T}) where {TE,T} timeevol = common_timeevol(sys1,sys2) A = [sys1.A sys1.B*sys2.C; - fill(zero(T), sys2.nx, sys1.nx) sys2.A] + zeros(T, sys2.nx, sys1.nx) sys2.A] B = [sys1.B*sys2.D ; sys2.B] C = [sys1.C sys1.D*sys2.C;] D = [sys1.D*sys2.D;] @@ -286,7 +288,7 @@ function *(sys1::HeteroStateSpace, sys2::HeteroStateSpace) timeevol = common_timeevol(sys1,sys2) T = promote_type(eltype(sys1.A),eltype(sys2.A)) A = [sys1.A sys1.B*sys2.C; - fill(zero(T), sys2.nx, sys1.nx) sys2.A] + zeros(T, sys2.nx, sys1.nx) sys2.A] B = [sys1.B*sys2.D ; sys2.B] C = [sys1.C sys1.D*sys2.C;] D = [sys1.D*sys2.D;] @@ -322,6 +324,7 @@ Base.ndims(::AbstractStateSpace) = 2 # NOTE: Also for SISO systems? Base.size(sys::AbstractStateSpace) = (noutputs(sys), ninputs(sys)) # NOTE: or just size(sys.D) Base.size(sys::AbstractStateSpace, d::Integer) = d <= 2 ? size(sys)[d] : 1 Base.eltype(::Type{S}) where {S<:AbstractStateSpace} = S +Base.axes(sys::AbstractStateSpace, i::Integer) = Base.OneTo(size(sys, i)) function Base.getindex(sys::ST, inds...) where ST <: AbstractStateSpace if size(inds, 1) != 2 diff --git a/src/types/TransferFunction.jl b/src/types/TransferFunction.jl index 6695ba9bd..d02acb591 100644 --- a/src/types/TransferFunction.jl +++ b/src/types/TransferFunction.jl @@ -33,6 +33,8 @@ ninputs(G::TransferFunction) = size(G.matrix, 2) Base.ndims(::TransferFunction) = 2 Base.size(G::TransferFunction) = size(G.matrix) Base.eltype(::Type{S}) where {S<:TransferFunction} = S +Base.zero(G::TransferFunction{TE,S}) where {TE,S} = tf(zeros(numeric_type(S), size(G)), G.timeevol) # can not create a zero of a discrete system from the type alone, the sampletime is not stored. + function Base.getindex(G::TransferFunction{TE,S}, inds...) where {TE,S<:SisoTf} if size(inds, 1) != 2 diff --git a/src/types/conversion.jl b/src/types/conversion.jl index 839df3c43..701fdb1ba 100644 --- a/src/types/conversion.jl +++ b/src/types/conversion.jl @@ -78,13 +78,12 @@ Base.convert(::Type{HeteroStateSpace}, s::StateSpace) = HeteroStateSpace(s) Base.convert(::Type{StateSpace}, s::HeteroStateSpace) = StateSpace(s.A, s.B, s.C, s.D, s.Ts) Base.convert(::Type{StateSpace}, s::HeteroStateSpace{Continuous}) = StateSpace(s.A, s.B, s.C, s.D) -function Base.convert(::Type{StateSpace}, G::TransferFunction{TE,<:SisoTf{T0}}) where {TE,T0<:Number} - +function Base.convert(::Type{StateSpace}, G::TransferFunction{TE,<:SisoTf{T0}}; kwargs...) where {TE,T0<:Number} T = Base.promote_op(/,T0,T0) - convert(StateSpace{TE,T}, G) + convert(StateSpace{TE,T}, G; kwargs...) end -function Base.convert(::Type{StateSpace{TE,T}}, G::TransferFunction) where {TE,T<:Number} +function Base.convert(::Type{StateSpace{TE,T}}, G::TransferFunction; balance=false) where {TE,T<:Number} if !isproper(G) error("System is improper, a state-space representation is impossible") end @@ -114,7 +113,9 @@ function Base.convert(::Type{StateSpace{TE,T}}, G::TransferFunction) where {TE,T A[inds,inds], B[inds,j:j], C[i:i,inds], D[i:i,j:j] = abcd_vec[k] end end - # A, B, C = balance_statespace(A, B, C)[1:3] NOTE: Use balance? + if balance + A, B, C = balance_statespace(A, B, C)[1:3] + end return StateSpace{TE,T}(A, B, C, D, TE(G.timeevol)) end @@ -172,7 +173,7 @@ function balance_statespace(A::AbstractMatrix, B::AbstractMatrix, C::AbstractMat @warn "Unable to balance state-space, returning original system" return A,B,C,I end - end +end # # First try to promote and hopefully get some types we can work with # function balance_statespace(A::AbstractMatrix, B::AbstractMatrix, C::AbstractMatrix, perm::Bool=false) @@ -240,7 +241,7 @@ end balance_transform(sys::StateSpace, perm::Bool=false) = balance_transform(sys.A,sys.B,sys.C,perm) -convert(::Type{TransferFunction}, sys::AbstractStateSpace{TE}) where TE = convert(TransferFunction{TE,SisoRational}, sys) +convert(::Type{TransferFunction}, sys::AbstractStateSpace{TE}) where TE = convert(TransferFunction{TE,SisoRational{numeric_type(sys)}}, sys) function convert(::Type{TransferFunction{TE,SisoRational{T}}}, sys::AbstractStateSpace) where {TE,T<:Number} matrix = Matrix{SisoRational{T}}(undef, size(sys)) @@ -284,7 +285,7 @@ Convert get zpk representation of sys from input j to output i function siso_ss_to_zpk(sys, i, j) A, B, C = struct_ctrb_obsv(sys.A, sys.B[:, j:j], sys.C[i:i, :]) D = sys.D[i:i, j:j] - z = tzero(A, B, C, D) + z = tzeros(A, B, C, D) nx = size(A, 1) nz = length(z) k = nz == nx ? D[1] : (C*(A^(nx - nz - 1))*B)[1] diff --git a/src/types/lqg.jl b/src/types/lqg.jl deleted file mode 100644 index 26cce312a..000000000 --- a/src/types/lqg.jl +++ /dev/null @@ -1,277 +0,0 @@ - -import Base.getindex - -""" - G = LQG(sys::AbstractStateSpace, Q1, Q2, R1, R2; qQ=0, qR=0, integrator=false, M = sys.C) - -Return an LQG object that describes the closed control loop around the process `sys=ss(A,B,C,D)` -where the controller is of LQG-type. The controller is specified by weight matrices `Q1,Q2` -that penalizes state deviations and control signal variance respectively, and covariance -matrices `R1,R2` which specify state drift and measurement covariance respectively. -This constructor calls [`lqr`](@ref) and [`kalman`](@ref) and forms the closed-loop system. - -If `integrator=true`, the resulting controller will have integral action. -This is achieved by adding a model of a constant disturbance on the inputs to the system -described by `sys`. - -`qQ` and `qR` can be set to incorporate loop transfer recovery, i.e., -```julia -L = lqr(A, B, Q1+qQ*C'C, Q2) -K = kalman(A, C, R1+qR*B*B', R2) -``` - -`M` is a matrix that defines the controlled variables `z`, if none is provided, the default is to consider all measured outputs `y` of the system as controlled. The definitions of `z` and `y` are given below -``` -y = C*x -z = M*x -``` - -# Fields and properties -When the LQG-object is populated by the lqg-function, the following fields have been made available -- `L` is the feedback matrix, such that `A-BL` is stable. Note that the length of the state vector (and the width of L) is increased by the number of inputs if the option `integrator=true`. -- `K` is the kalman gain such that `A-KC` is stable -- `sysc` is a dynamical system describing the controller `u=L*inv(A-BL-KC+KDL)Ky` - -Several other properties of the object are accessible as properties. The available properties are -(some have many alternative names, separated with / ) - -- `G.cl / G.closedloop` is the closed-loop system, including observer, from reference to output, precompensated to have static gain 1 (`u = −Lx + lᵣr`). -- `G.S / G.Sin` Input sensitivity function -- `G.T / G.Tin` Input complementary sensitivity function -- `G.Sout` Output sensitivity function -- `G.Tout` Output complementary sensitivity function -- `G.CS` The transfer function from measurement noise to control signal -- `G.DS` The transfer function from input load disturbance to output -- `G.lt / G.looptransfer / G.loopgain = PC` -- `G.rd / G.returndifference = I + PC` -- `G.sr / G.stabilityrobustness = I + inv(PC)` -- `G.sysc / G.controller` Returns the controller as a StateSpace-system - -It is also possible to access all fileds using the `G.symbol` syntax, the fields are `P,Q1,Q2,R1,R2,qQ,qR,sysc,L,K,integrator` - -# Example - -```julia -s = tf("s") -P = [1/(s+1) 2/(s+2); 1/(s+3) 1/(s-1)] -sys = ss(P) -eye(n) = Matrix{Float64}(I,n,n) # For convinience - -qQ = 1 -qR = 1 -Q1 = 10eye(4) -Q2 = 1eye(2) -R1 = 1eye(6) -R2 = 1eye(2) - -G = LQG(sys, Q1, Q2, R1, R2, qQ=qQ, qR=qR, integrator=true) - -Gcl = G.cl -T = G.T -S = G.S -sigmaplot([S,T],exp10.(range(-3, stop=3, length=1000))) -stepplot(Gcl) -``` - -""" -struct LQG - P::StateSpace - Q1::AbstractMatrix - Q2::AbstractMatrix - R1::AbstractMatrix - R2::AbstractMatrix - qQ::Real - qR::Real - sysc::LTISystem - L::AbstractMatrix - K::AbstractMatrix - M::AbstractMatrix - integrator::Bool -end - -# Provide some constructors -function LQG( - sys::LTISystem, - Q1::AbstractMatrix, - Q2::AbstractMatrix, - R1::AbstractMatrix, - R2::AbstractMatrix; - qQ = 0, - qR = 0, - integrator = false, - kwargs..., -) - integrator ? _LQGi(sys, Q1, Q2, R1, R2, qQ, qR; kwargs...) : - _LQG(sys, Q1, Q2, R1, R2, qQ, qR; kwargs...) -end # (1) Dispatches to final - -function LQG( - sys::LTISystem, - Q1::AbstractVector, - Q2::AbstractVector, - R1::AbstractVector, - R2::AbstractVector; - qQ = 0, - qR = 0, - integrator = false, - kwargs..., -) - Q1 = diagm(0 => Q1) - Q2 = diagm(0 => Q2) - R1 = diagm(0 => R1) - R2 = diagm(0 => R2) - integrator ? _LQGi(sys, Q1, Q2, R1, R2, qQ, qR; kwargs...) : - _LQG(sys, Q1, Q2, R1, R2, qQ, qR; kwargs...) -end # (2) Dispatches to final - - -# This function does the actual initialization in the standard case withput integrator -function _LQG(sys::LTISystem, Q1, Q2, R1, R2, qQ, qR; M = sys.C) - A, B, C, D = ssdata(sys) - n = size(A, 1) - m = size(B, 2) - p = size(C, 1) - L = lqr(A, B, Q1 + qQ * C'C, Q2) - K = kalman(A, C, R1 + qR * B * B', R2) - - # Controller system - Ac = A - B*L - K*C + K*D*L - Bc = K - Cc = L - Dc = zero(D') - sysc = ss(Ac, Bc, Cc, Dc) - - return LQG(ss(A, B, C, D), Q1, Q2, R1, R2, qQ, qR, sysc, L, K, M, false) -end - - -# This function does the actual initialization in the integrator case -function _LQGi(sys::LTISystem, Q1, Q2, R1, R2, qQ, qR; M = sys.C) - A, B, C, D = ssdata(sys) - n = size(A, 1) - m = size(B, 2) - p = size(C, 1) - - # Augment with disturbance model - Ae = [A B; zeros(m, n + m)] - Be = [B; zeros(m, m)] - Ce = [C zeros(p, m)] - De = D - - L = lqr(A, B, Q1 + qQ * C'C, Q2) - Le = [L I] - K = kalman(Ae, Ce, R1 + qR * Be * Be', R2) - - # Controller system - Ac = Ae - Be*Le - K*Ce + K*De*Le - Bc = K - Cc = Le - Dc = zero(D') - sysc = ss(Ac, Bc, Cc, Dc) - - LQG(ss(A, B, C, D), Q1, Q2, R1, R2, qQ, qR, sysc, Le, K, M, true) -end - -@deprecate getindex(G::LQG, s::Symbol) getfield(G, s) - -function Base.getproperty(G::LQG, s::Symbol) - if s ∈ (:L, :K, :Q1, :Q2, :R1, :R2, :qQ, :qR, :integrator, :P, :M) - return getfield(G, s) - end - s === :A && return G.P.A - s === :B && return G.P.B - s === :C && return G.P.C - s === :D && return G.P.D - s ∈ (:sys, :P) && return getfield(G, :P) - s ∈ (:sysc, :controller) && return getfield(G, :sysc) - - A = G.P.A - B = G.P.B - C = G.P.C - D = G.P.D - M = G.M - - L = G.L - K = G.K - P = G.P - sysc = G.sysc - - n = size(A, 1) - m = size(B, 2) - p = size(C, 1) - pm = size(M, 1) - - # Extract interesting values - if G.integrator # Augment with disturbance model - A = [A B; zeros(m, n + m)] - B = [B; zeros(m, m)] - C = [C zeros(p, m)] - M = [M zeros(pm, m)] - D = D - end - - PC = P * sysc # Loop gain - - if s ∈ (:cl, :closedloop, :ry) # Closed-loop system - # Compensate for static gain, pp. 264 G.L. - dcg = P.C * ((P.B * L[:, 1:n] - P.A) \ P.B) - Acl = [A-B*L B*L; zero(A) A-K*C] - Bcl = [B / dcg; zero(B)] - Ccl = [M zero(M)] - # rank(dcg) == size(A,1) && (Bcl = Bcl / dcg) # B*lᵣ # Always normalized with nominal plant static gain - syscl = ss(Acl, Bcl, Ccl, 0) - # return ss(A-B*L, B/dcg, M, 0) - return syscl - elseif s ∈ (:Sin, :S) # Sensitivity function - return feedback(ss(Matrix{numeric_type(PC)}(I, m, m)), PC) - elseif s ∈ (:Tin, :T) # Complementary sensitivity function - return feedback(PC) - # return ss(Acl, I(size(Acl,1)), Ccl, 0)[1,2] - elseif s === :Sout # Sensitivity function, output - return feedback(ss(Matrix{numeric_type(sysc)}(I, m, m)), sysc * P) - elseif s === :Tout # Complementary sensitivity function, output - return feedback(sysc * P) - elseif s === :PS # Load disturbance to output - return P * G.S - elseif s === :CS # Noise to control signal - return sysc * G.S - elseif s ∈ (:lt, :looptransfer, :loopgain) - return PC - elseif s ∈ (:rd, :returndifference) - return ss(Matrix{numeric_type(PC)}(I, p, p)) + PC - elseif s ∈ (:sr, :stabilityrobustness) - return ss(Matrix{numeric_type(PC)}(I, p, p)) + inv(PC) - end - error("The symbol $s does not have a function associated with it.") -end - -Base.:(==)(G1::LQG, G2::LQG) = - G1.K == G2.K && G1.L == G2.L && G1.P == G2.P && G1.sysc == G2.sysc - - -plot(G::LQG) = gangoffourplot(G) - -function gangoffour(G::LQG) - G.S, G.PS, G.CS, G.T -end - -function gangoffourplot(G::LQG; kwargs...) - S,D,N,T = gangoffour(G) - f1 = sigmaplot(S, show=false, kwargs...); Plots.plot!(title="\$S = 1/(1+PC)\$") - f2 = sigmaplot(D, show=false, kwargs...); Plots.plot!(title="\$D = P/(1+PC)\$") - f3 = sigmaplot(N, show=false, kwargs...); Plots.plot!(title="\$N = C/(1+PC)\$") - f4 = sigmaplot(T, show=false, kwargs...); Plots.plot!(title="\$T = PC/(1+PC\$)") - Plots.plot(f1,f2,f3,f4) -end - - - -# function gangoffourplot(G::LQG, args...) -# S,D,N,T = gangoffour(G) -# fig = subplot(n=4,nc=2) -# Plots.plot!(fig[1,1],sigmaplot(S, args...), title="\$S = 1/(1+PC)\$") -# Plots.plot!(fig[1,2],sigmaplot(D, args...), title="\$D = P/(1+PC)\$") -# Plots.plot!(fig[2,1],sigmaplot(N, args...), title="\$N = C/(1+PC)\$") -# Plots.plot!(fig[2,2],sigmaplot(T, args...), title="\$T = PC/(1+PC\$)") -# return fig -# end diff --git a/src/types/promotion.jl b/src/types/promotion.jl index 0f4b4b7b0..421cc0cec 100644 --- a/src/types/promotion.jl +++ b/src/types/promotion.jl @@ -65,7 +65,7 @@ Base.promote_rule(::Type{TransferFunction{TE, SisoRational{T1}}}, ::Type{MT}) wh TransferFunction{TE, SisoRational{promote_type(T1, T2)}} Base.promote_rule(::Type{StateSpace{TE, T1}}, ::Type{MT}) where {TE, T1, MT<:AbstractMatrix} = - StateSpace{TE, promote_type(T,eltype(MT))} + StateSpace{TE, promote_type(T1,eltype(MT))} Base.promote_rule(::Type{DelayLtiSystem{T1,S}}, ::Type{MT}) where {T1, S, MT<:AbstractMatrix} = DelayLtiSystem{promote_type(T1, eltype(MT)),S} diff --git a/src/types/result_types.jl b/src/types/result_types.jl new file mode 100644 index 000000000..df84af793 --- /dev/null +++ b/src/types/result_types.jl @@ -0,0 +1,43 @@ +abstract type AbstractResult end # Common for all result types, e.g., SimResult and FreqRespResult + +## SimResult: the output of lsim etc. ========================================== +abstract type AbstractSimResult <: AbstractResult end # Result of a time-domain simulation + +struct SimResult{Ty, Tt, Tx, Tu, Ts} <: AbstractSimResult # Result of lsim + y::Ty + t::Tt + x::Tx + u::Tu + sys::Ts +end + +# To emulate, e.g., lsim(sys, u)[1] -> y +function Base.getindex(r::SimResult, i::Int) + return getfield(r, i) +end + +function Base.getindex(r::SimResult, v::AbstractVector) + return getfield.((r,), v) +end + +# to allow destructuring, e.g., y,t,x = lsim(sys, u) +# This performs explicit iteration in the type domain to ensure inferability +Base.iterate(r::SimResult) = (r.y, Val(:t)) +Base.iterate(r::SimResult, ::Val{:t}) = (r.t, Val(:x)) +Base.iterate(r::SimResult, ::Val{:x}) = (r.x, Val(:u)) +Base.iterate(r::SimResult, ::Val{:u}) = (r.u, Val(:done)) +Base.iterate(r::SimResult, ::Val{:done}) = nothing + + +function Base.getproperty(r::SimResult, s::Symbol) + s ∈ fieldnames(SimResult) && (return getfield(r, s)) + if s === :nx + return size(r.x, 1) + elseif s === :nu + return size(r.u, 1) + elseif s === :ny + return size(r.y, 1) + else + throw(ArgumentError("Unsupported property $s")) + end +end \ No newline at end of file diff --git a/src/types/tf.jl b/src/types/tf.jl index be833b4f1..726f727b2 100644 --- a/src/types/tf.jl +++ b/src/types/tf.jl @@ -66,7 +66,7 @@ tf(D::AbstractArray{T}) where T = tf(D, Continuous()) tf(n::Number, args...; kwargs...) = tf([n], args...; kwargs...) -tf(sys::AbstractStateSpace) = convert(TransferFunction, sys) # NOTE: Would perhaps like to write TransferFunction{SisoRational}, but couldn't get this to work... +tf(sys::AbstractStateSpace{TE}) where TE = convert(TransferFunction{TE, SisoRational{float(numeric_type(sys))}}, sys) # the call to float is required since an eigenvalue-computation is performed that produces floats from Ints etc. function tf(G::TransferFunction{TE,<:SisoTf{T}}) where {TE<:TimeEvolution,T<:Number} convert(TransferFunction{TE,SisoRational{T}}, G) diff --git a/src/types/zpk.jl b/src/types/zpk.jl index 47edb609b..594860282 100644 --- a/src/types/zpk.jl +++ b/src/types/zpk.jl @@ -44,21 +44,15 @@ function zpk(z::AbstractVector{TZ}, p::AbstractVector{TP}, k::T, Ts::TE) where { end function zpk(z::AbstractVector, p::AbstractVector, k::T, Ts::TE) where {TE<:TimeEvolution, T<:Number} # To be able to send in empty vectors [] of type Any - if eltype(z) == Any && eltype(p) == Any - @assert z == [] - @assert p == [] - return zpk(T[], T[], k, Ts) - elseif eltype(z) == Any - @assert z == [] - TR = eltype(p) - return zpk(TR[], p, k, Ts) - elseif eltype(p) == Any - @assert p == [] - TR = eltype(z) - return zpk(z, TR[], k, Ts) - else - error("Non numeric vectors must be empty.") + if eltype(z) == Any + z == [] || throw(ArgumentError("non numeric vectors must be empty.")) + z = T[] end + if eltype(p) == Any + p == [] || throw(ArgumentError("non numeric vectors must be empty.")) + p = T[] + end + zpk(z, p, k, Ts) end function zpk(gain::Matrix{T}, Ts::TE; kwargs...) where {TE<:TimeEvolution, T <: Number} diff --git a/src/utilities.jl b/src/utilities.jl index c877a1a8d..ca63b60c9 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -8,6 +8,7 @@ numeric_type(::Type{TransferFunction{TE,S}}) where {TE,S} = numeric_type(S) numeric_type(::Type{<:StateSpace{TE,T}}) where {TE,T} = T numeric_type(::Type{<:HeteroStateSpace{TE,AT}}) where {TE,AT} = eltype(AT) numeric_type(::Type{<:DelayLtiSystem{T}}) where {T} = T +numeric_type(sys::AbstractStateSpace) = eltype(sys.A) numeric_type(sys::LTISystem) = numeric_type(typeof(sys)) @@ -27,30 +28,16 @@ function to_abstract_matrix(A::AbstractArray) end return A end -to_abstract_matrix(A::Vector) = reshape(A, length(A), 1) +to_abstract_matrix(A::AbstractVector) = reshape(A, length(A), 1) to_abstract_matrix(A::Number) = fill(A, 1, 1) # Do no sorting of eigenvalues -@static if VERSION > v"1.2.0-DEV.0" - eigvalsnosort(args...; kwargs...) = eigvals(args...; sortby=nothing, kwargs...) - roots(args...; kwargs...) = Polynomials.roots(args...; sortby=nothing, kwargs...) -else - eigvalsnosort(args...; kwargs...) = eigvals(args...; kwargs...) - roots(args...; kwargs...) = Polynomials.roots(args...; kwargs...) -end +eigvalsnosort(args...; kwargs...) = eigvals(args...; sortby=nothing, kwargs...) +roots(args...; kwargs...) = Polynomials.roots(args...; sortby=nothing, kwargs...) issemiposdef(A) = ishermitian(A) && minimum(real.(eigvals(A))) >= 0 issemiposdef(A::UniformScaling) = real(A.λ) >= 0 -@static if VERSION < v"1.1.0-DEV" - #Added in 1.1.0-DEV - LinearAlgebra.isposdef(A::UniformScaling) = isposdef(A.λ) -end -@static if VERSION < v"1.1" - isnothing(::Any) = false - isnothing(::Nothing) = true -end - """ f = printpolyfun(var) `fun` Prints polynomial in descending order, with variable `var` """ @@ -77,7 +64,7 @@ function roots2real_poly_factors(roots::Vector{cT}) where cT <: Number end if k == length(roots) || r != conj(roots[k+1]) - throw(AssertionError("Found pole without matching conjugate.")) + throw(ArgumentError("Found pole without matching conjugate.")) end push!(poly_factors,Polynomial{T}([real(r)^2+imag(r)^2, -2*real(r), 1])) diff --git a/test/framework.jl b/test/framework.jl index 569f474d5..a5789867d 100644 --- a/test/framework.jl +++ b/test/framework.jl @@ -1,3 +1,15 @@ +# Set plot globals +ENV["PLOTS_TEST"] = "true" +ENV["GKSwstype"] = "nul" + +using Plots +gr() +default(show=false) + +using ControlSystems +# Local definition to make sure we get warnings if we use eye +eye_(n) = Matrix{Int64}(I, n, n) + # Length not defined for StateSpace, so use custom function function Test.test_approx_eq(va::StateSpace, vb::StateSpace, Eps, astr, bstr) fields = [:timeevol, :nx, :ny, :nu, :inputnames, :outputnames, :statenames] diff --git a/test/runtests.jl b/test/runtests.jl index f006fe24d..5c0c54ea1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,10 +5,8 @@ import SparseArrays: sparse # In test_matrix_comps import DSP: conv # In test_conversion and test_synthesis include("framework.jl") -# Local definition to make sure we get warnings if we use eye -eye_(n) = Matrix{Int64}(I, n, n) - my_tests = [ + "test_result_types", "test_timeevol", "test_statespace", "test_transferfunction", @@ -24,7 +22,6 @@ my_tests = [ "test_timeresp", "test_analysis", "test_matrix_comps", - "test_lqg", "test_synthesis", "test_pid_design", "test_partitioned_statespace", diff --git a/test/test_analysis.jl b/test/test_analysis.jl index 866e230ad..76f0742f2 100644 --- a/test/test_analysis.jl +++ b/test/test_analysis.jl @@ -1,5 +1,5 @@ @testset "test_analysis" begin -## TZERO ## +## tzeros ## # Examples from the Emami-Naeini & Van Dooren Paper # Example 3 A = [0 1 0 0 0 0; @@ -16,7 +16,7 @@ D = [1 0; 1 0] ex_3 = ss(A, B, C, D) -@test tzero(ex_3) ≈ [0.3411639019140099 + 1.161541399997252im, +@test tzeros(ex_3) ≈ [0.3411639019140099 + 1.161541399997252im, 0.3411639019140099 - 1.161541399997252im, 0.9999999999999999 + 0.0im, -0.6823278038280199 + 0.0im] @@ -35,13 +35,13 @@ C = [1 0 0 0 0; 0 1 0 0 0] D = zeros(2, 2) ex_4 = ss(A, B, C, D) -@test tzero(ex_4) ≈ [-0.06467751189940692,-0.3680512036036696] +@test tzeros(ex_4) ≈ [-0.06467751189940692,-0.3680512036036696] # Example 5 s = tf("s") ex_5 = 1/s^15 -@test tzero(ex_5) == Float64[] -@test tzero(ss(ex_5)) == Float64[] +@test tzeros(ex_5) == Float64[] +@test tzeros(ss(ex_5)) == Float64[] # Example 6 A = [2 -1 0; @@ -51,13 +51,13 @@ B = [0; 0; 1] C = [0 -1 0] D = [0] ex_6 = ss(A, B, C, D) -@test tzero(ex_6) == Float64[] +@test tzeros(ex_6) == Float64[] @test ss(A, [0 0 1]', C, D) == ex_6 # Example 7 ex_7 = ss(zeros(2, 2), [0;1], [-1 0], [0]) -@test tzero(ex_7) == Float64[] +@test tzeros(ex_7) == Float64[] # Example 8 A = [-2 1 0 0 0 0; @@ -71,12 +71,12 @@ C = [0 0 0 1 0 0] D = [0] ex_8 = ss(A, B, C, D) # TODO : there may be a way to improve the precision of this example. -@test tzero(ex_8) ≈ [-1.0, -1.0] atol=1e-7 +@test tzeros(ex_8) ≈ [-1.0, -1.0] atol=1e-7 # Example 9 ex_9 = (s - 20)/s^15 -@test tzero(ex_9) ≈ [20.0] -@test tzero(ss(ex_9)) ≈ [20.0] +@test tzeros(ex_9) ≈ [20.0] +@test tzeros(ss(ex_9)) ≈ [20.0] # Example 11 A = [-2 -6 3 -7 6; @@ -93,20 +93,21 @@ D = [0 0; 0 0; 0 0] ex_11 = ss(A, B, C, D) -@test tzero(ex_11) ≈ [4.0, -3.0] +@test tzeros(ex_11) ≈ [4.0, -3.0] # Test for multiple zeros, siso tf +s = tf("s") sys = s*(s + 1)*(s^2 + 1)*(s - 3)/((s + 1)*(s + 4)*(s - 4)) -@test tzero(sys) ≈ [3.0, -1.0, im, -im, 0.0] +@test tzeros(sys) ≈ [3.0, -1.0, im, -im, 0.0] ## POLE ## -@test pole(sys) ≈ [4.0, -4.0, -1.0] -@test pole([sys sys]) ≈ [4.0, -4.0, -1.0] # Issue #81 -@test pole(ex_11) ≈ eigvals(ex_11.A) -@test pole([2/(s+1) 3/(s+2); 1/(s+1) 1/(s+1)]) ≈ [-1, -1, -2] +@test poles(sys) ≈ [4.0, -4.0, -1.0] +@test poles([sys sys]) ≈ [4.0, -4.0, -1.0] # Issue #81 +@test poles(ex_11) ≈ eigvals(ex_11.A) +@test poles([2/(s+1) 3/(s+2); 1/(s+1) 1/(s+1)]) ≈ [-1, -1, -2] -poles = [-3.383889568918823 + 0.000000000000000im +known_poles = [-3.383889568918823 + 0.000000000000000im -2.199935841931115 + 0.000000000000000im -0.624778101910111 + 1.343371895589931im -0.624778101910111 - 1.343371895589931im @@ -116,13 +117,13 @@ approxin2(el,col) = any(el.≈col) # Compares the computed poles with the expected poles # TODO: Improve the test for testing equalifity of sets of complex numbers # i.e. simplify and handle doubles. -@test all(approxin(p,poles) for p in pole(ex_8)) && all(approxin2(p,pole(ex_8)) for p in poles) +@test all(approxin(p,known_poles) for p in poles(ex_8)) && all(approxin2(p,poles(ex_8)) for p in known_poles) ex_12 = ss(-3, 2, 1, 2) -@test pole(ex_12) ≈ [-3] +@test poles(ex_12) ≈ [-3] ex_13 = ss([-1 1; 0 -1], [0; 1], [1 0], 0) -@test pole(ex_13) ≈ [-1, -1] +@test poles(ex_13) ≈ [-1, -1] ## ZPKDATA ## @@ -149,6 +150,8 @@ z, p, k = zpkdata(G) ## GAIN ## #Gain is confusing when referring to zpkdata. Test dcgain instead @test [dcgain(H[1, 1]) dcgain(H[1, 2]); dcgain(H[2, 1]) dcgain(H[2, 2])] ≈ [0 0; 0.2 1/3] @test [dcgain(G[1, 1]) dcgain(G[1, 2]); dcgain(G[2, 1]) dcgain(G[2, 2])] ≈ [0 0; 0.2 1/3] +@test dcgain(H[1, 1], 1e-6)[] == 0 +@test dcgain(H[2, 1], 1e-6)[] ≈ 0.2 rtol=1e-5 ## MARKOVPARAM ## @test markovparam(G, 0) == [0.0 0.0; 1.0 0.0] @@ -172,27 +175,18 @@ damp_output = damp(ex_11) ## DAMPREPORT ## -@test sprint(dampreport, sys) == ( - "| Pole | Damping | Frequency | Time Constant |\n"* - "| | Ratio | (rad/sec) | (sec) |\n"* - "+---------------+---------------+---------------+---------------+\n"* - "| -1.000e+00 | 1.000e+00 | 1.000e+00 | 1.000e+00 |\n"* - "| 4.000e+00 | -1.000e+00 | 4.000e+00 | -2.500e-01 |\n"* - "| -4.000e+00 | 1.000e+00 | 4.000e+00 | 2.500e-01 |\n") - -@test sprint(dampreport, 1/(s+1+2im)/(s+2+3im)) == ( - "| Pole | Damping | Frequency | Time Constant |\n"* - "| | Ratio | (rad/sec) | (sec) |\n"* - "+---------------+---------------+---------------+---------------+\n"* - "| -1.000e+00 | 4.472e-01 | 2.236e+00 | 1.000e+00 |\n"* - "| -2.000e+00 im| | | |\n"* - "| -2.000e+00 | 5.547e-01 | 3.606e+00 | 5.000e-01 |\n"* - "| -3.000e+00 im| | | |\n") +s = tf("s") +sys = s*(s + 1)*(s^2 + 1)*(s - 3)/((s + 1)*(s + 4)*(s - 4)) +@test sprint(dampreport, sys) == "| Pole | Damping | Frequency | Frequency | Time Constant |\n| | Ratio | (rad/sec) | (Hz) | (sec) |\n+--------------------+---------------+---------------+---------------+---------------+\n| -1 | 1 | 1 | 0.159 | 1 |\n| +4 | -1 | 4 | 0.637 | -0.25 |\n| -4 | 1 | 4 | 0.637 | 0.25 |\n" + +@test sprint(dampreport, ex_4) == "| Pole | Damping | Frequency | Frequency | Time Constant |\n| | Ratio | (rad/sec) | (Hz) | (sec) |\n+--------------------+---------------+---------------+---------------+---------------+\n| +0 | -1 | 0 | 0 | -Inf |\n| -0.0597 ± 0.0171im | 0.961 | 0.0621 | 0.00989 | 16.7 |\n| -0.0858 | 1 | 0.0858 | 0.0137 | 11.7 |\n| -0.18 | 1 | 0.18 | 0.0287 | 5.55 |\n" + +@test sprint(dampreport, 1/(s+1+2im)/(s+2+3im)) == "| Pole | Damping | Frequency | Frequency | Time Constant |\n| | Ratio | (rad/sec) | (Hz) | (sec) |\n+--------------------+---------------+---------------+---------------+---------------+\n| -1 -2im | 0.447 | 2.24 | 0.356 | 1 |\n| -2 -3im | 0.555 | 3.61 | 0.574 | 0.5 |\n" # Example 5.5 from http://www.control.lth.se/media/Education/EngineeringProgram/FRTN10/2017/e05_both.pdf G = [1/(s+2) -1/(s+2); 1/(s+2) (s+1)/(s+2)] -@test_broken length(pole(G)) == 1 -@test length(tzero(G)) == 1 +@test_broken length(poles(G)) == 1 +@test length(tzeros(G)) == 1 @test_broken size(minreal(ss(G)).A) == (2,2) diff --git a/test/test_complex.jl b/test/test_complex.jl index 97be230c3..96b572a22 100644 --- a/test/test_complex.jl +++ b/test/test_complex.jl @@ -14,8 +14,8 @@ C_2 = zpk([-1+im], [], 1.0+1im) @test im*ss(1) == ss(im) -@test pole(zpk([], [-1+im,-1+im,0], 2.0im)) == [-1+im,-1+im,0] -@test tzero(zpk([-1+im,-1+im,0], [-2], 2.0im)) == [-1+im,-1+im,0] +@test poles(zpk([], [-1+im,-1+im,0], 2.0im)) == [-1+im,-1+im,0] +@test tzeros(zpk([-1+im,-1+im,0], [-2], 2.0im)) == [-1+im,-1+im,0] @test zpk( tf([1.0, 1+im], [1.0, 2+im]) ) == zpk( [-1-im], [-2-im], 1.0+0im) @@ -23,7 +23,7 @@ C_2 = zpk([-1+im], [], 1.0+1im) @test minreal(zpk([-1+im, -1+im], [-1+im],1+1im)) == zpk([-1+im], [], 1+1im) -@test_throws AssertionError zpk([-1+im], [-1+im,-1+im],1) # Given the type of k this should be a real-coefficient system, but poles and zeros don't come in conjugate pairs +@test_throws ArgumentError zpk([-1+im], [-1+im,-1+im],1) # Given the type of k this should be a real-coefficient system, but poles and zeros don't come in conjugate pairs @test zpk([-2+im], [-1+im],1+0im)*zpk([], [-1+im],1+0im) == zpk([-2+im], [-1+im, -1+im], 1+0im) @test zpk([], [-2], 2) + zpk([], [-1], 1) == zpk([-4/3], [-2,-1], 3) @@ -33,10 +33,10 @@ C_2 = zpk([-1+im], [], 1.0+1im) @test 1 / ( tf("s") + 1 + im ) == tf([1], [1, 1+im]) s = tf("s"); -@test tzero(ss(-1, 1, 1, 1.0im)) ≈ [-1.0 + im] rtol=1e-15 -@test tzero(ss([-1.0-im 1-im; 2 0], [2; 0], [-1+1im -0.5-1.25im], 1)) ≈ [-1-2im, 2-im] +@test tzeros(ss(-1, 1, 1, 1.0im)) ≈ [-1.0 + im] rtol=1e-15 +@test tzeros(ss([-1.0-im 1-im; 2 0], [2; 0], [-1+1im -0.5-1.25im], 1)) ≈ [-1-2im, 2-im] -@test tzero(ss((s-2.0-1.5im)^3/(s+1+im)/(s+2)^3)) ≈ fill(2.0 + 1.5im, 3) rtol=1e-4 -@test tzero(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)^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 end diff --git a/test/test_connections.jl b/test/test_connections.jl index 19247697e..fbc38dbb7 100644 --- a/test/test_connections.jl +++ b/test/test_connections.jl @@ -83,6 +83,15 @@ s = tf("s") vecarray(2, 1, [1,13,55,75], [1,13,55,75])); @test parallel(Ctf_111, Ctf_211) == tf([2,17,44,45], [1,13,55,75]) +# Test that the additive identity element for LTI system is known +@test zero(C_111) isa typeof(C_111) +@test zero(Ctf_111) isa typeof(Ctf_111) +@test zero(ss(randn(2,3))) == ss(zeros(2,3)) +@test zero(tf(randn(2,3))) == tf(zeros(2,3)) + + + + # Combination tf and ss @test [C_111 Ctf_221] == [C_111 ss(Ctf_221)] @test [C_111; Ctf_212] == [C_111; ss(Ctf_212)] @@ -224,7 +233,7 @@ K1d = ss(-1, 1, 1, 0, 1) @test feedback(0.5, G3, pos_feedback=false) ≈ ss(-4/3, 1/3, -1/3, 1/3) @test feedback(0.5, G3, pos_feedback=true) ≈ ss(0, 1, 1, 1) -@test_broken feedback(G3, 1) == ss(-1.5, 0.5, 0.5, 0.5) # Old feedback method +@test feedback(G3, 1) == ss(-1.5, 0.5, 0.5, 0.5) # Old feedback method @test feedback(G3, 1, pos_feedback=false) == ss(-1.5, 0.5, 0.5, 0.5) # Test that errors are thrown for mismatched dimensions @@ -246,7 +255,7 @@ K1d = ss(-1, 1, 1, 0, 1) G4 = ss(-6, [7 8], [11; 12], 0) @test starprod(G1, G4, 1, 1) == ss([-9 33; 35 -6], [2 0; 0 8], [4 0; 0 12], zeros(2,2)) - +# TRANSFER FUNCTIONS # Feedback2dof @@ -258,4 +267,15 @@ F = tf(1.0, [1,1]) @test_nowarn feedback2dof(P0, C, F) + +G1 = tf([1, 0],[1, 2, 2]) +G2 = tf([1, 1, 2],[1, 0, 3]) +@test feedback(G1, G2) == tf([1, 0, 3, 0], [1, 3, 6, 8, 6]) +G1 = tf(1.0) +G2 = tf(1.0, [1, 5]) +@test feedback(G1, G2) == tf([1, 5], [1, 6]) +@test feedback(1, G2) == tf([1, 5], [1, 6]) +@test feedback(G2, G1) == tf(1, [1, 6]) +@test feedback(G2, 1) == tf(1, [1, 6]) + end diff --git a/test/test_conversion.jl b/test/test_conversion.jl index a9e9a364e..ebde7234e 100644 --- a/test/test_conversion.jl +++ b/test/test_conversion.jl @@ -61,7 +61,7 @@ sys2 = convert(TransferFunction, sys) w = 10.0 .^ (-2:2:50) @test freqresp(sys, w) ≈ freqresp(sys2, w) csort = v -> sort(v, lt = (x,y) -> abs(x) < abs(y)) -@test csort(pole(zpk(sys2))) ≈ [1+im, -2.0-3im] +@test csort(poles(zpk(sys2))) ≈ [1+im, -2.0-3im] # Test complex 2 sys4 = ss([-1.0+im 0;1 1],[1;0],[1 im],im) diff --git a/test/test_delayed_systems.jl b/test/test_delayed_systems.jl index 1547c9258..f53d852fa 100644 --- a/test/test_delayed_systems.jl +++ b/test/test_delayed_systems.jl @@ -10,18 +10,19 @@ import DelayDiffEq: MethodOfSteps, Tsit5 @test typeof(promote(delay(0.2), ss(1.0 + im))[1]) == DelayLtiSystem{Complex{Float64}, Float64} -if VERSION >= v"1.6.0-DEV.0" - @test sprint(show, ss(1,1,1,1)*delay(1.0)) == "DelayLtiSystem{Float64, Float64}\n\nP: StateSpace{Continuous, Float64}\nA = \n 1.0\nB = \n 0.0 1.0\nC = \n 1.0\n 0.0\nD = \n 0.0 1.0\n 1.0 0.0\n\nContinuous-time state-space model\n\nDelays: [1.0]\n" -else - @test sprint(show, ss(1,1,1,1)*delay(1.0)) == "DelayLtiSystem{Float64,Float64}\n\nP: StateSpace{Continuous,Float64}\nA = \n 1.0\nB = \n 0.0 1.0\nC = \n 1.0\n 0.0\nD = \n 0.0 1.0\n 1.0 0.0\n\nContinuous-time state-space model\n\nDelays: [1.0]\n" -end +@test sprint(show, ss(1,1,1,1)*delay(1.0)) == "DelayLtiSystem{Float64, Float64}\n\nP: StateSpace{Continuous, Float64}\nA = \n 1.0\nB = \n 0.0 1.0\nC = \n 1.0\n 0.0\nD = \n 0.0 1.0\n 1.0 0.0\n\nContinuous-time state-space model\n\nDelays: [1.0]" -# Extremely baseic tests +# Extremely basic tests @test freqresp(delay(1), ω) ≈ reshape(exp.(-im*ω), length(ω), 1, 1) rtol=1e-15 @test freqresp(delay(2.5), ω)[:] ≈ exp.(-2.5im*ω) rtol=1e-15 @test freqresp(3.5*delay(2.5), ω)[:] ≈ 3.5*exp.(-2.5im*ω) rtol=1e-15 @test freqresp(delay(2.5)*1.5, ω)[:] ≈ exp.(-2.5im*ω)*1.5 rtol=1e-15 +# Addition of constant +@test evalfr(1 + delay(1.0), 0)[] ≈ 2 +@test evalfr(1 - delay(1.0), 0)[] ≈ 0 +@test evalfr([2 -delay(1.0)], 0) ≈ [2 -1] + # Stritcly proper system P1 = DelayLtiSystem(ss(-1.0, 1, 1, 0)) P1_fr = 1 ./ (im*ω .+ 1) @@ -166,21 +167,21 @@ println("Simulating first delay system:") @time step(delay(1)*tf(1,[1,1])) @time y1, t1, x1 = step([s11;s12], 10) -@time @test y1[:,2] ≈ step(s12, t1)[1] rtol = 1e-14 +@time @test y1[2:2,:] ≈ step(s12, t1)[1] rtol = 1e-14 t = 0.0:0.1:10 y2, t2, x2 = step(s1, t) # TODO Figure out which is inexact here -@test y2[:,1,1:1] + y2[:,1,2:2] ≈ step(s11, t)[1] + step(s12, t)[1] rtol=1e-14 +@test y2[1:1,:,1:1] + y2[1:1,:,2:2] ≈ step(s11, t)[1] + step(s12, t)[1] rtol=1e-14 y3, t3, x3 = step([s11; s12], t) -@test y3[:,1,1] ≈ step(s11, t)[1] rtol = 1e-14 -@test y3[:,2,1] ≈ step(s12, t)[1] rtol = 1e-14 +@test y3[1:1,:,1] ≈ step(s11, t)[1] rtol = 1e-14 +@test y3[2:2,:,1] ≈ step(s12, t)[1] rtol = 1e-14 y1, t1, x1 = step(DelayLtiSystem([1.0/s 2/s; 3/s 4/s]), t) y2, t2, x2 = step([1.0/s 2/s; 3/s 4/s], t) @test y1 ≈ y2 rtol=1e-14 -@test size(x1,1) == length(t) +@test size(x1,2) == length(t) @test size(x1,3) == 2 @@ -202,7 +203,7 @@ function y_expected(t, K) end end -@test ystep ≈ y_expected.(t, K) atol = 1e-12 +@test ystep' ≈ y_expected.(t, K) atol = 1e-12 function dy_expected(t, K) if t < 1 @@ -219,13 +220,13 @@ end y_impulse, t, _ = impulse(sys_known, 3) # TODO Better accuracy for impulse -@test y_impulse ≈ dy_expected.(t, K) rtol=1e-13 -@test maximum(abs, y_impulse - dy_expected.(t, K)) < 1e-12 +@test y_impulse' ≈ dy_expected.(t, K) rtol=1e-13 +@test maximum(abs, y_impulse' - dy_expected.(t, K)) < 1e-12 y_impulse, t, _ = impulse(sys_known, 3, alg=MethodOfSteps(Tsit5())) # Two orders of magnitude better with BS3 in this case, which is default for impulse -@test y_impulse ≈ dy_expected.(t, K) rtol=1e-5 -@test maximum(abs, y_impulse - dy_expected.(t, K)) < 1e-5 +@test y_impulse' ≈ dy_expected.(t, K) rtol=1e-5 +@test maximum(abs, y_impulse' - dy_expected.(t, K)) < 1e-5 ## Test delay with D22 term t = 0:0.01:4 @@ -234,16 +235,16 @@ sys = delay(1) y, t, x = step(sys, t) @test y[:] ≈ [zeros(100); ones(301)] atol = 1e-12 -@test size(x) == (401,0) +@test size(x) == (0,401) sys = delay(1)*delay(1)*1/s y, t, x = step(sys, t) -y_sol = [zeros(200);0:0.01:2] +y_sol = [zeros(200);0:0.01:2]' @test maximum(abs,y-y_sol) < 1e-13 -@test maximum(abs,x-collect(0:0.01:4)) < 1e-15 +@test maximum(abs,x-collect(0:0.01:4)') < 1e-15 # TODO For some reason really bad accuracy here # Looks like a lag in time diff --git a/test/test_discrete.jl b/test/test_discrete.jl index 683d96bc1..25ffc52ae 100644 --- a/test/test_discrete.jl +++ b/test/test_discrete.jl @@ -43,6 +43,7 @@ C_222_d = ss([-5 -3; 2 -9], [1 0; 0 2], [1 0; 0 1], [1 0; 0 1]) # Test c2d for transfer functions G = tf([1, 1], [1, 3, 1]) Gd = c2d(G, 0.2) +@test typeof(Gd) <: TransferFunction{<:Discrete, <:ControlSystems.SisoRational} @test Gd ≈ tf([0, 0.165883310712090, -0.135903621603238], [1.0, -1.518831946985175, 0.548811636094027], 0.2) rtol=1e-14 # Issue #391 @@ -54,8 +55,9 @@ C_210 = ss(C_212.A, C_212.B, zeros(0, 2), zeros(0, 1)) @test c2d(C_210, 0.01).A ≈ c2d(C_212, 0.01).A # c2d on a zpk model should arguably return a zpk model -@test_broken typeof(c2d(zpk(G), 1)) <: TransferFunction{<:ControlSystems.SisoZpk} - +Gdzpk = c2d(zpk(G), 0.2) +@test typeof(Gdzpk) <: TransferFunction{<:Discrete, <:ControlSystems.SisoZpk} +@test Gdzpk ≈ zpk([0.8192724211968178], [0.9264518519788194, 0.5923800950063556], 0.16588331071209, 0.2) rtol=1e-14 # ERRORS @@ -64,25 +66,48 @@ C_210 = ss(C_212.A, C_212.B, zeros(0, 2), zeros(0, 1)) # d2c -@static if VERSION > v"1.4" # log(matrix) is buggy on previous versions, should be fixed in 1.4 and back-ported to 1.0.6 - @test d2c(c2d(C_111, 0.01)) ≈ C_111 - @test d2c(c2d(C_212, 0.01)) ≈ C_212 - @test d2c(c2d(C_221, 0.01)) ≈ C_221 - @test d2c(c2d(C_222_d, 0.01)) ≈ C_222_d - @test d2c(Gd) ≈ G - - sys = ss([0 1; 0 0], [0;1], [1 0], 0) - sysd = c2d(sys, 1) - @test d2c(sysd) ≈ sys -end +@test d2c(c2d(C_111, 0.01)) ≈ C_111 +@test d2c(c2d(C_212, 0.01)) ≈ C_212 +@test d2c(c2d(C_221, 0.01)) ≈ C_221 +@test d2c(c2d(C_222_d, 0.01)) ≈ C_222_d +@test d2c(Gd) ≈ G + +sys = ss([0 1; 0 0], [0;1], [1 0], 0) +sysd = c2d(sys, 1) +@test d2c(sysd) ≈ sys -# forward euler + +# forward euler / tustin @test c2d(C_111, 1, :fwdeuler).A == I + C_111.A -@test d2c(c2d(C_111, 0.01, :fwdeuler), :fwdeuler) ≈ C_111 -@test d2c(c2d(C_212, 0.01, :fwdeuler), :fwdeuler) ≈ C_212 -@test d2c(c2d(C_221, 0.01, :fwdeuler), :fwdeuler) ≈ C_221 -@test d2c(c2d(C_222_d, 0.01, :fwdeuler), :fwdeuler) ≈ C_222_d -@test d2c(c2d(G, 0.01, :fwdeuler), :fwdeuler) ≈ G +for method in (:fwdeuler, :tustin) + @test d2c(c2d(C_111, 0.01, method), method) ≈ C_111 atol = sqrt(eps()) + @test d2c(c2d(C_212, 0.01, method), method) ≈ C_212 atol = sqrt(eps()) + @test d2c(c2d(C_221, 0.01, method), method) ≈ C_221 atol = sqrt(eps()) + @test d2c(c2d(C_222_d, 0.01, method), method) ≈ C_222_d atol = sqrt(eps()) + @test d2c(c2d(G, 0.01, method), method) ≈ G atol = sqrt(eps()) +end + +matlab_tustin = let + A = [0.95094630230333 -0.02800401390866; 0.018669342605773 0.913607617091783] + B = [0.009754731511517 -0.000280040139087; 9.3346713029e-5 0.019136076170918] + C = [0.975473151151665 -0.01400200695433; 0.009334671302887 0.956803808545892] + D = [1.004877365755758 -0.000140020069543; 4.6673356514e-5 1.009568038085459] + ss(A,B,C,D,0.01) +end + +sys_tustin = c2d(C_222_d, 0.01, :tustin) +@test sys_tustin ≈ matlab_tustin atol = 1e-12 + +matlab_prewarp = let + A = [0.950906169509825 -0.028025790680969; 0.018683860453979 0.913538448601866] + B = [0.009762667760265 -0.00028049168885; 9.3497229617e-5 0.019151346602063] + C = [0.975453084754912 -0.014012895340485; 0.00934193022699 0.956769224300933] + D = [1.004881333880133 -0.000140245844425; 4.6748614808e-5 1.009575673301031] + sys = ss(A,B,C,D,0.01) +end + +sys_prewarp = c2d(C_222_d, 0.01, :tustin, w_prewarp=10) +@test sys_prewarp ≈ matlab_prewarp atol = 1e-12 Cd = c2d(C_111, 0.001, :fwdeuler) @@ -115,11 +140,11 @@ Gcl = tf(conv(B,T),zpconv(A,R,B,S)) # Form the closed loop polynomial from refer @test ControlSystems.isstable(Gcl) -p = pole(Gcl) +p = poles(Gcl) # Test that all desired poles are in the closed-loop system -@test norm(minimum(abs.((pole(tf(Bm,Am)) .- sort(p, by=imag)')), dims=2)) < 1e-6 +@test norm(minimum(abs.((poles(tf(Bm,Am)) .- sort(p, by=imag)')), dims=2)) < 1e-6 # Test that the observer poles are in the closed-loop system -@test norm(minimum(abs.((pole(tf(1,Ao)) .- sort(p, by=imag)')), dims=2)) < 1e-6 +@test norm(minimum(abs.((poles(tf(1,Ao)) .- sort(p, by=imag)')), dims=2)) < 1e-6 end diff --git a/test/test_lqg.jl b/test/test_lqg.jl deleted file mode 100644 index c4ca4deee..000000000 --- a/test/test_lqg.jl +++ /dev/null @@ -1,46 +0,0 @@ - -@testset "test_lqg" begin - - -w = exp10.(range(-5, stop=5, length=1000)) -s = tf("s") -P = [1/(s+1) 2/(s+3); 1/(s+1) 1/(s+1)] -sys = ss(P) -sysmin = minreal(sys) -A,B,C,D = sys.A,sys.B,sys.C,sys.D -Am,Bm,Cm,Dm = sysmin.A,sysmin.B,sysmin.C,sysmin.D - -@test approxsetequal(eigvals(Am), [-3,-1,-1]) - -Q1 = 100eye_(4) -Q2 = 1eye_(2) -R1 = 100eye_(4) -R2 = 1eye_(2) -G = LQG(sys, Q1, Q2, R1, R2) -gangoffourplot(G) # Test that it at least does not error -@test approxsetequal(eigvals(G.sysc.A), [ -31.6209+0.0im, -1.40629+0.0im, -15.9993+0.911174im, -15.9993-0.911174im, ], rtol = 1e-3) - -qQ = 1 -qR = 1 -Q1 = 1000eye_(4) -Q2 = 1eye_(2) -R1 = 1eye_(6) -R2 = 1eye_(2) -Gi = LQG(sys, Q1, Q2, R1, R2, qQ=qQ, qR=qR, integrator=true) -gangoffourplot(Gi) # Test that it at least does not error -@test approxsetequal(eigvals(Gi.sysc.A), [0.0, 0.0, -47.4832, -44.3442, -3.40255, -1.15355 ], rtol = 1e-3) - -@test approxsetequal(eigvals(G.cl.A), [-1.0, -14.1774, -2.21811, -14.3206, -1.60615, -22.526, -1.0, -14.1774], rtol=1e-3) -@test approxsetequal(eigvals(G.T.A), [-22.526, -2.21811, -1.60615, -14.3206, -14.1774, -1.0, -1.0, -14.1774], rtol=1e-3) -@test approxsetequal(eigvals(G.S.A), [-22.526, -2.21811, -1.60615, -14.3206, -14.1774, -1.0, -1.0, -14.1774], rtol=1e-3) -@test approxsetequal(eigvals(G.CS.A), [-31.6209+0.0im, -1.40629+0.0im, -15.9993+0.911174im, -15.9993-0.911174im, -22.526+0.0im, -2.21811+0.0im, -1.60615+0.0im, -14.3206+0.0im, -14.1774+0.0im, -1.0+0.0im, -1.0+0.0im, -14.1774+0.0im], rtol=1e-3) -@test approxsetequal(eigvals(G.PS.A), [-1.0, -1.0, -3.0, -1.0, -22.526, -2.21811, -1.60615, -14.3206, -14.1774, -1.0, -1.0, -14.1774], rtol=1e-3) - - -@test approxsetequal(eigvals(Gi.cl.A), [-1.0, -44.7425, -44.8455, -2.23294, -4.28574, -2.06662, -0.109432, -1.31779, -0.78293, -1.0, 0.0, 0.0], rtol=1e-3) -@test approxsetequal(eigvals(Gi.T.A), [-44.7425, -44.8455, -4.28574, -0.109432, -2.23294, -2.06662, -1.31779, -0.78293, -1.0, -1.0], rtol=1e-3) -@test approxsetequal(eigvals(Gi.S.A), [-44.7425, -44.8455, -4.28574, -0.109432, -2.23294, -2.06662, -1.31779, -0.78293, -1.0, -1.0], rtol=1e-3) - - - -end diff --git a/test/test_matrix_comps.jl b/test/test_matrix_comps.jl index 93ada018a..b22ed9445 100644 --- a/test/test_matrix_comps.jl +++ b/test/test_matrix_comps.jl @@ -8,7 +8,7 @@ sysr, G = balreal(sys) @test gram(sysr, :c) ≈ G @test gram(sysr, :o) ≈ G -@test sort(pole(sysr)) ≈ sort(pole(sys)) +@test sort(poles(sysr)) ≈ sort(poles(sys)) sysb,T = ControlSystems.balance_statespace(sys) Ab,Bb,Cb,T = ControlSystems.balance_statespace(A,B,C) @@ -105,4 +105,32 @@ sysi = ControlSystems.innovation_form(sys, sysw=sysw) @test sysi.B ≈ [4.01361818808572 40.26132476965486] -end + +# Test observer_predictor +sysp = ControlSystems.observer_predictor(sys, I(2), I(1)) +K = kalman(sys, I(2), I(1)) +@test sysp.A == sys.A-K*sys.C +@test sysp.B == [sys.B-K*sys.D K] + + +# Test observer_controller +sys = ssrand(2,3,4) +Q1 = I(4) +Q2 = I(3) +R1 = I(4) +R2 = I(2) +L = lqr(sys, Q1, Q2) +K = kalman(sys, R1, R2) +cont = observer_controller(sys, L, K) +syscl = feedback(sys, cont) + +pcl = poles(syscl) +A,B,C,D = ssdata(sys) +allpoles = [ + eigvals(A-B*L) + eigvals(A-K*C) +] +@test sort(pcl, by=LinearAlgebra.eigsortby) ≈ sort(allpoles, by=LinearAlgebra.eigsortby) +@test cont.B == K + +end diff --git a/test/test_pid_design.jl b/test/test_pid_design.jl index af0d62a22..5e0ac8b1f 100644 --- a/test/test_pid_design.jl +++ b/test/test_pid_design.jl @@ -23,4 +23,9 @@ kp,ki,C = loopshapingPI(P,10; phasemargin = 30, doplot = false) _,_,_,pm = margin(P*C) @test pm[] > 30 +P = tf(1,[1, 1]) +piparams,C = placePI(P, 2, 0.7) +@test poles(feedback(P, C)) ≈ [-1.4 + √2.04im, -1.4 - √2.04im] +@test [piparams[:Kp], piparams[:Ti]] ≈ [9/5, 9/20] + end diff --git a/test/test_plots.jl b/test/test_plots.jl index a06301afb..ff5a5c07d 100644 --- a/test/test_plots.jl +++ b/test/test_plots.jl @@ -1,11 +1,3 @@ -# Set plot globals -ENV["PLOTS_TEST"] = "true" -ENV["GKSwstype"] = "nul" - -using Plots -gr() -default(show=false) - # This function show mirror that in ControlExamplePlots.jl/genplots.jl # to make sure that the plots in these tests can be tested for accuracy """funcs, names = getexamples() @@ -28,10 +20,10 @@ function getexamples() #Only siso for now nicholsgen() = nicholsplot(tf1,ws) - stepgen() = stepplot(sys, ts[end], l=(:dash, 4)) - impulsegen() = impulseplot(sys, ts[end], l=:blue) + stepgen() = plot(step(sys, ts[end]), l=(:dash, 4)) + impulsegen() = plot(impulse(sys, ts[end]), l=:blue) L = lqr(sysss.A, sysss.B, [1 0; 0 1], [1 0; 0 1]) - lsimgen() = lsimplot(sysss, (x,i)->-L*x, ts, [1;2]) + lsimgen() = plot(lsim(sysss, (x,i)->-L*x, ts; x0=[1;2]), plotu=true) margingen() = marginplot([tf1, tf2], ws) gangoffourgen() = begin diff --git a/test/test_result_types.jl b/test/test_result_types.jl new file mode 100644 index 000000000..66817f799 --- /dev/null +++ b/test/test_result_types.jl @@ -0,0 +1,37 @@ +@testset "SimResult" begin + @info "Testing SimResult" + + import ControlSystems: SimResult, ssrand + + y = randn(1,10) + u = randn(1,10) + x = randn(1,10) + t = 1:10 + sys = ssrand(1, 1, 1, Ts=1) + + r = SimResult(y,t,x,u,sys) + + # test getindex + @test r[1] == y + @test r[2] == t + @test r[3] == x + @test r[4] == u + @test r[5] == sys + + # test destructuring + y2,t2,x2,u2 = r + @test y2 === y + @test t2 === t + @test x2 === x + @test u2 === u + + # test inferability of destructuring + foo(r::SimResult) = (a,b,c,d) = r + @inferred foo(r) + + # test properties + @test r.nx == 1 + @test r.nu == 1 + @test r.ny == 1 +end + diff --git a/test/test_simplification.jl b/test/test_simplification.jl index 54ddefcd1..159dd61ef 100644 --- a/test/test_simplification.jl +++ b/test/test_simplification.jl @@ -8,6 +8,23 @@ G = ss([-5 0 0 0; 0 -1 -2.5 0; 0 4 0 0; 0 0 0 -6], [2 0; 0 1; 0 0; 0 2], @test sminreal(G[2, 1]) == ss([-5], [2], [-2], [1]) @test sminreal(G[2, 2]) == ss([-6], [2], [1], [0]) +using ControlSystems.DemoSystems: resonant +R = resonant()*resonant() +@test sminreal(R) == R # https://github.com/JuliaControl/ControlSystems.jl/issues/409 + +# https://github.com/JuliaControl/ControlSystems.jl/issues/475 +A = [ + 0.0 0.0 0.0; + 1.0 0.0 -0.00578297; + 0.0 1.0 0.0; +] +B = [1.0; 0.0; 0.0] +C = [0.0 0.0 1.0] +D = [0] + +ss_sys = ss(A,B,C,D) +@test sminreal(ss_sys) == ss_sys + ## MINREAL ## @@ -22,7 +39,7 @@ sysmin = minreal(sys) @test_broken balreal(sys-sysmin) -@test all(sigma(sys-sysmin, [0.0, 1.0, 2.0])[1] .< 1e-15) # Previously crashed because of zero dimensions in tzero +@test all(sigma(sys-sysmin, [0.0, 1.0, 2.0])[1] .< 1e-15) # Previously crashed because of zero dimensions in tzeros t = 0:0.1:10 y1,x1 = step(sys,t)[[1,3]] diff --git a/test/test_statespace.jl b/test/test_statespace.jl index b049211f8..d9caf7019 100644 --- a/test/test_statespace.jl +++ b/test/test_statespace.jl @@ -85,6 +85,7 @@ @test C_222[1:1,1] == SS([-5 -3; 2 -9],[1; 0],[1 0],[0]) @test C_222[1,1:2] == C_221 @test size(C_222[1,[]]) == (1,0) + @test C_222[end, end] == C_222[2,2] A = [-1.0 -2.0; 0.0 -1.0] @@ -111,21 +112,10 @@ # Printing if SS <: StateSpace - if VERSION >= v"1.6.0-DEV.0" - @test sprint(show, C_222) == "StateSpace{Continuous, Int64}\nA = \n -5 -3\n 2 -9\nB = \n 1 0\n 0 2\nC = \n 1 0\n 0 1\nD = \n 0 0\n 0 0\n\nContinuous-time state-space model" - @test sprint(show, C_022) == "StateSpace{Continuous, Float64}\nD = \n 4.0 0.0\n 0.0 4.0\n\nContinuous-time state-space model" - @test sprint(show, D_022) == "StateSpace{Discrete{Float64}, Float64}\nD = \n 4.0 0.0\n 0.0 4.0\n\nSample Time: 0.005 (seconds)\nDiscrete-time state-space model" - @test sprint(show, D_222) == "StateSpace{Discrete{Float64}, Float64}\nA = \n 0.2 -0.8\n -0.8 0.07\nB = \n 1.0 0.0\n 0.0 2.0\nC = \n 1.0 0.0\n 0.0 1.0\nD = \n 0.0 0.0\n 0.0 0.0\n\nSample Time: 0.005 (seconds)\nDiscrete-time state-space model" - else - @test sprint(show, C_222) == "StateSpace{Continuous,Int64}\nA = \n -5 -3\n 2 -9\nB = \n 1 0\n 0 2\nC = \n 1 0\n 0 1\nD = \n 0 0\n 0 0\n\nContinuous-time state-space model" - @test sprint(show, C_022) == "StateSpace{Continuous,Float64}\nD = \n 4.0 0.0\n 0.0 4.0\n\nContinuous-time state-space model" - @test sprint(show, D_022) == "StateSpace{Discrete{Float64},Float64}\nD = \n 4.0 0.0\n 0.0 4.0\n\nSample Time: 0.005 (seconds)\nDiscrete-time state-space model" - if VERSION > v"1.4.0-DEV.0" # Spurious blank space in matrix_print_row was removed in #33298 - @test sprint(show, D_222) == "StateSpace{Discrete{Float64},Float64}\nA = \n 0.2 -0.8\n -0.8 0.07\nB = \n 1.0 0.0\n 0.0 2.0\nC = \n 1.0 0.0\n 0.0 1.0\nD = \n 0.0 0.0\n 0.0 0.0\n\nSample Time: 0.005 (seconds)\nDiscrete-time state-space model" - else - @test sprint(show, D_222) == "StateSpace{Discrete{Float64},Float64}\nA = \n 0.2 -0.8 \n -0.8 0.07\nB = \n 1.0 0.0\n 0.0 2.0\nC = \n 1.0 0.0\n 0.0 1.0\nD = \n 0.0 0.0\n 0.0 0.0\n\nSample Time: 0.005 (seconds)\nDiscrete-time state-space model" - end - end + @test sprint(show, C_222) == "StateSpace{Continuous, Int64}\nA = \n -5 -3\n 2 -9\nB = \n 1 0\n 0 2\nC = \n 1 0\n 0 1\nD = \n 0 0\n 0 0\n\nContinuous-time state-space model" + @test sprint(show, C_022) == "StateSpace{Continuous, Float64}\nD = \n 4.0 0.0\n 0.0 4.0\n\nContinuous-time state-space model" + @test sprint(show, D_022) == "StateSpace{Discrete{Float64}, Float64}\nD = \n 4.0 0.0\n 0.0 4.0\n\nSample Time: 0.005 (seconds)\nDiscrete-time state-space model" + @test sprint(show, D_222) == "StateSpace{Discrete{Float64}, Float64}\nA = \n 0.2 -0.8\n -0.8 0.07\nB = \n 1.0 0.0\n 0.0 2.0\nC = \n 1.0 0.0\n 0.0 1.0\nD = \n 0.0 0.0\n 0.0 0.0\n\nSample Time: 0.005 (seconds)\nDiscrete-time state-space model" end # Errors diff --git a/test/test_synthesis.jl b/test/test_synthesis.jl index 457077a86..9c35927f1 100644 --- a/test/test_synthesis.jl +++ b/test/test_synthesis.jl @@ -15,7 +15,7 @@ T = [1] @test isapprox(numpoly(minreal(feedback(L),1e-5))[1].coeffs, numpoly(tf(1,[1,1]))[1].coeffs)# This test is ugly, but numerical stability is poor for minreal @test feedback2dof(B,A,R,S,T) == tf(B.*T, conv(A,R) + [0;0;conv(B,S)]) @test feedback2dof(P,R,S,T) == tf(B.*T, conv(A,R) + [0;0;conv(B,S)]) -@test isapprox(pole(minreal(tf(feedback(Lsys)),1e-5)) , pole(minreal(feedback(L),1e-5)), atol=1e-5) +@test isapprox(poles(minreal(tf(feedback(Lsys)),1e-5)) , poles(minreal(feedback(L),1e-5)), atol=1e-5) Pint = tf(1,[1,1]) Cint = tf([1,1],[1,0]) @@ -23,10 +23,9 @@ Lint = P*C @test isapprox(minreal(feedback(Pint,Cint),1e-5), tf([1,0],[1,2,1]), rtol = 1e-5) # TODO consider keeping minreal of Int system Int @test isapprox(numpoly(minreal(feedback(Lint),1e-5))[1].coeffs, numpoly(tf(1,[1,1]))[1].coeffs)# This test is ugly, but numerical stability is poor for minreal -@test isapprox(pole(minreal(tf(feedback(Lsys)),1e-5)) , pole(minreal(feedback(L),1e-5)), atol=1e-5) +@test isapprox(poles(minreal(tf(feedback(Lsys)),1e-5)) , poles(minreal(feedback(L),1e-5)), atol=1e-5) - -@test_throws ErrorException feedback(ss(1),ss(1)) +@test feedback(ss(1),ss(1)) == ss(0.5) @test_throws ErrorException feedback(ss([1 0; 0 1], ones(2,2), ones(1,2),0)) # Test Feedback Issue: 163 @@ -57,6 +56,38 @@ z5,p5,k5 = zpkdata(ffb5) end + +@testset "place" begin + sys = ss(-4, 2, 3, 0) + A, B, C, _ = ssdata(sys) + + @test place(A, B, [-10]) == [3][:,:] + @test place(A, B, [-10], :c) == [3][:,:] + @test place(A, C, [-10], :o) == [2][:,:] + + A = [0 1; 0 0] + B = [0; 1] + C = [1 0] + sys = ss(A, B, C, 0) + + @test place(A, B, [-1.0, -1]) ≈ [1 2] + @test place(sys, [-1.0, -1]) ≈ [1 2] + @test place(A, B, [-1.0, -1], :c) ≈ [1 2] + @test place(sys, [-1.0, -1], :c) ≈ [1 2] + @test place(A, C, [-2.0, -2], :o) ≈ [4; 4] + @test place(sys, [-2.0, -2], :o) ≈ [4; 4] + + @test place(A, B, [-2 + im, -2 - im]) ≈ [5 4] + @test place(A, C, [-4 + 2im, -4 - 2im], :o) ≈ [8; 20] + + A = ones(3,3) - diagm([3, 4, 5]) + B = [1; 0; 2] + C = [1 1 0] + @test place(A, B, [-2 + 2im, -2 - 2im, -4]) ≈ [-2.6 5.2 0.8] + @test place(A, C, [-2 + 3im, -2 - 3im, -4], :o) ≈ [11; -12; 1] +end + + @testset "acker" begin Random.seed!(0) A = randn(3,3) diff --git a/test/test_timeresp.jl b/test/test_timeresp.jl index c629a2946..c59020537 100644 --- a/test/test_timeresp.jl +++ b/test/test_timeresp.jl @@ -16,10 +16,10 @@ x0 = [1.,0] y, t, x, uout = lsim(sys,u,t,x0=x0) # Continuous time th = 1e-6 -@test sum(abs.(x[end,:])) < th +@test sum(abs.(x[:,end])) < th y, t, x, uout = lsim(c2d(sys,0.1),u,t,x0=x0) # Discrete time -@test sum(abs.(x[end,:])) < th +@test sum(abs.(x[:,end])) < th #Do a manual simulation with uout ym, tm, xm = lsim(sys, uout, t, x0=x0) @@ -32,53 +32,53 @@ sysd = c2d(sys, 0.1) # Create the closed loop system sysdfb = ss(sysd.A-sysd.B*L, sysd.B, sysd.C, sysd.D, 0.1) #Simulate without input -yd, td, xd = lsim(sysdfb, zeros(501), t, x0=x0) +yd, td, xd = lsim(sysdfb, zeros(1, 501), t, x0=x0) @test y ≈ yd @test x ≈ xd # Error for nonuniformly spaced vector -@test_throws ErrorException lsim(sys, [1, 2, 3, 4], [0, 1, 1, 2]) +@test_throws ErrorException lsim(sys, [1 2 3 4], [0, 1, 1, 2]) # Error for mismatch of sample rates of discrete-system and signal -@test_throws ErrorException lsim(sysd, [1, 2, 3, 4], 0:0.2:0.6) +@test_throws ErrorException lsim(sysd, [1 2 3 4], 0:0.2:0.6) # Test for problem with broadcast dimensions -@test lsim(sys, zeros(5), 0:0.2:0.8)[1][:] == zeros(5) +@test lsim(sys, zeros(1, 5), 0:0.2:0.8)[1][:] == zeros(5) # lsim for Int system with Float64 input (regression test for #264) -@test lsim(ss(1,1,1,1,1), ones(5), 0:4)[1][:] == 1:5 +@test lsim(ss(1,1,1,1,1), ones(1, 5), 0:4)[1][:] == 1:5 # Various combinations of BigFloat -@test lsim(ss(big.(1),1,1,1,1), ones(5), 0:4)[1][:] == 1:5 -@test lsim(ss(big.(1),1,1,1,1), big.(ones(5)), 0:4)[1][:] == 1:5 -@test lsim(ss(1,1,1,1,1), big.(ones(5)), 0:4)[1][:] == 1:5 +@test lsim(ss(big.(1),1,1,1,1), ones(1, 5), 0:4)[1][:] == 1:5 +@test lsim(ss(big.(1),1,1,1,1), big.(ones(1, 5)), 0:4)[1][:] == 1:5 +@test lsim(ss(1,1,1,1,1), big.(ones(1, 5)), 0:4)[1][:] == 1:5 # Tests for HeteroStateSpace -@test lsim(HeteroStateSpace(big.(1.0),1,1,1,1), ones(5), 0:4)[1][:] == 1:5 +@test lsim(HeteroStateSpace(big.(1.0),1,1,1,1), ones(1, 5), 0:4)[1][:] == 1:5 # lsim for discrete-time complex-coefficient systems # Complex system, real input signal G1 = ss(1.0im, 1) -@test lsim(G1, [1, 2], 0:1)[1][:] == 1.0im*[1, 2] +@test lsim(G1, [1 2], 0:1)[1][:] == 1.0im*[1, 2] @test impulse(G1, 4)[1][:] == [1.0im; zeros(4)] @test step(G1, 4)[1][:] == fill(1.0im, 5) G2 = ss(1.0im, 1, 1, 0, 1) -@test lsim(G2, ones(3), 0:2)[1][:] == [0.0, 1, 1 + im] +@test lsim(G2, ones(1, 3), 0:2)[1][:] == [0.0, 1, 1 + im] @test impulse(G2, 4)[1][:] == [0.0, 1, im, -1, -im] @test step(G2, 4)[1][:] == [0.0, 1, 1+im, 1+im-1, 0] # Real system, complex input signal -@test lsim(ss(1, 1), [1.0, 1.0im], 0:1)[1][:] == [1.0, 1.0im] -@test lsim(ss(1.0, 1), [1.0, 1.0im], 0:1)[1][:] == [1.0, 1.0im] -@test lsim(ss(1, 1, 1, 0, 1), [1.0, 1im, 1], 0:2)[1][:] == [0.0, 1, 1 + im] +@test lsim(ss(1, 1), [1.0 1.0im], 0:1)[1][:] == [1.0, 1.0im] +@test lsim(ss(1.0, 1), [1.0 1.0im], 0:1)[1][:] == [1.0, 1.0im] +@test lsim(ss(1, 1, 1, 0, 1), [1.0 1im 1], 0:2)[1][:] == [0.0, 1, 1 + im] # Complex system, complex input signal -@test lsim(ss(1.0im, 1), [1im, 2], 0:1)[1][:] == [-1, 2im] -@test lsim(ss(1.0im, 1, 1, 0, 1), [1.0, 1im, 1], 0:2)[1][:] == [0.0, 1, 2im] +@test lsim(ss(1.0im, 1), [1im 2], 0:1)[1][:] == [-1, 2im] +@test lsim(ss(1.0im, 1, 1, 0, 1), [1.0 1im 1], 0:2)[1][:] == [0.0, 1, 2im] # Test that the discrete lsim accepts u function that returns scalar @@ -89,7 +89,7 @@ yd, td, xd = lsim(sysd, u, t, x0=x0) @test norm(x - xd)/norm(x) < 0.05 # Test lsim with default time vector -uv = randn(length(t)) +uv = randn(1, length(t)) y,t = lsim(c2d(sys,0.1),uv,t,x0=x0) yd,td = lsim(c2d(sys,0.1),uv,x0=x0) @test yd == y @@ -99,79 +99,79 @@ yd,td = lsim(c2d(sys,0.1),uv,x0=x0) t0 = 0:0.05:2 systf = [tf(1,[1,1]) 0; 0 tf([1,-1],[1,2,1])] sysss = ss([-1 0 0; 0 -2 -1; 0 1 0], [1 0; 0 1; 0 0], [1 0 0; 0 1 -1], 0) -yreal = zeros(length(t0), 2, 2) -xreal = zeros(length(t0), 3, 2) +yreal = zeros(2, length(t0), 2) +xreal = zeros(3, length(t0), 2) #Step tf y, t, x = step(systf, t0) -yreal[:,1,1] = 1 .- exp.(-t) -yreal[:,2,2] = -1 .+ exp.(-t)+2*exp.(-t).*t +yreal[1,:,1] = 1 .- exp.(-t) +yreal[2,:,2] = -1 .+ exp.(-t)+2*exp.(-t).*t @test y ≈ yreal atol=1e-4 #Step ss y, t, x = step(sysss, t) @test y ≈ yreal atol=1e-4 -xreal[:,1,1] = yreal[:,1,1] -xreal[:,2,2] = exp.(-t).*t -xreal[:,3,2] = exp.(-t).*(-t .- 1) .+ 1 +xreal[1,:,1] = yreal[1,:,1] +xreal[2,:,2] = exp.(-t).*t +xreal[3,:,2] = exp.(-t).*(-t .- 1) .+ 1 @test x ≈ xreal atol=1e-5 #Impulse tf y, t, x = impulse(systf, t) -yreal[:,1,1] = exp.(-t) -yreal[:,2,2] = exp.(-t).*(1 .- 2*t) +yreal[1,:,1] = exp.(-t) +yreal[2,:,2] = exp.(-t).*(1 .- 2*t) @test y ≈ yreal atol=1e-2 #Impulse ss y, t, x = impulse(sysss, t) @test y ≈ yreal atol=1e-2 -xreal[:,1,1] = yreal[:,1,1] -xreal[:,2,2] = -exp.(-t).*t + exp.(-t) -xreal[:,3,2] = exp.(-t).*t +xreal[1,:,1] = yreal[1,:,1] +xreal[2,:,2] = -exp.(-t).*t + exp.(-t) +xreal[3,:,2] = exp.(-t).*t @test x ≈ xreal atol=1e-1 #Test step and impulse Discrete t0 = 0:0.05:2 systf = [tf(1,[1,1]) 0; 0 tf([1,-1],[1,2,1])] sysss = ss([-1 0 0; 0 -2 -1; 0 1 0], [1 0; 0 1; 0 0], [1 0 0; 0 1 -1], 0) -yreal = zeros(length(t0), 2, 2) -xreal = zeros(length(t0), 3, 2) +yreal = zeros(2, length(t0), 2) +xreal = zeros(3, length(t0), 2) #Step tf y, t, x = step(systf, t0, method=:zoh) -yreal[:,1,1] = 1 .- exp.(-t) -yreal[:,2,2] = -1 .+ exp.(-t) + 2*exp.(-t).*t +yreal[1,:,1] = 1 .- exp.(-t) +yreal[2,:,2] = -1 .+ exp.(-t) + 2*exp.(-t).*t @test y ≈ yreal atol=1e-14 #Step ss y, t, x = step(sysss, t, method=:zoh) @test y ≈ yreal atol=1e-13 -xreal[:,1,1] = yreal[:,1,1] -xreal[:,2,2] = exp.(-t).*t -xreal[:,3,2] = exp.(-t).*(-t .- 1) .+ 1 +xreal[1,:,1] = yreal[1,:,1] +xreal[2,:,2] = exp.(-t).*t +xreal[3,:,2] = exp.(-t).*(-t .- 1) .+ 1 @test x ≈ xreal atol=1e-14 #Impulse tf y, t, x = impulse(systf, t, method=:zoh) -yreal[:,1,1] = exp.(-t) -yreal[:,2,2] = exp.(-t).*(1 .- 2*t) +yreal[1,:,1] = exp.(-t) +yreal[2,:,2] = exp.(-t).*(1 .- 2*t) @test y ≈ yreal atol=1e-14 #Impulse ss y, t, x = impulse(1.0sysss, t, method=:zoh) @test y ≈ yreal atol=1e-13 -xreal[:,1,1] = yreal[:,1,1] -xreal[:,2,2] = -exp.(-t).*t + exp.(-t) -xreal[:,3,2] = exp.(-t).*t +xreal[1,:,1] = yreal[1,:,1] +xreal[2,:,2] = -exp.(-t).*t + exp.(-t) +xreal[3,:,2] = exp.(-t).*t @test x ≈ xreal atol=1e-13 #Step response of discrete system with specified final time G = tf([1], [1; zeros(3)], 1) y, t2, x = step(G, 10) -@test y ≈ [zeros(3); ones(8)] atol=1e-5 +@test y ≈ [zeros(1, 3) ones(1, 8)] atol=1e-5 @test t2 == 0:1:10 # isapprox is broken for ranges (julia 1.3.1) #Impulse response of discrete system to final time that is not mulitple of the sample time G = tf([1], [1; zeros(3)], 0.3) y, t2, x = step(G, 2) -@test y ≈ [zeros(3); ones(4)] atol=1e-5 +@test y ≈ [zeros(1, 3) ones(1, 4)] atol=1e-5 @test t2 ≈ 0:0.3:1.8 atol=1e-5 #Make sure t was never changed @@ -208,7 +208,7 @@ s = Simulator(P, reference) x0 = [0.,0] tspan  = (0.0,tfinal) sol = solve(s, x0, tspan, OrdinaryDiffEq.Tsit5()) -@test step(P,t)[3] ≈ reduce(hcat,sol.(t))' +@test step(P,t)[3] ≈ reduce(hcat,sol.(t)) end end diff --git a/test/test_zpk.jl b/test/test_zpk.jl index 53c9a7108..7bdfb433d 100644 --- a/test/test_zpk.jl +++ b/test/test_zpk.jl @@ -43,7 +43,7 @@ z = zpk("z", 0.005) @test zpk([], [1.0+im,1.0,1.0-im], 1.0) == zpk(ComplexF64[], [1.0+im,1.0-im,1.0], 1.0) @test zpk([1.0-im,1.0,1.0+im], [], 1.0) == zpk([1.0+im,1.0-im,1.0], ComplexF64[], 1.0) @test zpk([], [1.0+im,1.0,1.0+im,1.0-im,1.0-im], 1.0) == zpk(ComplexF64[], [1.0+im,1.0-im,1.0+im,1.0-im,1.0], 1.0) -@test_throws AssertionError zpk([], [1.01+im,1.0-im], 1.0) +@test_throws ArgumentError zpk([], [1.01+im,1.0-im], 1.0) #TODO improve polynomial accuracy se these are equal