From ba4db83ed41c1c4037366d8f85255a3172d74a6b Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Mon, 11 Jul 2022 09:16:00 +0200 Subject: [PATCH 1/2] make tf broadcast as ss --- README.md | 1 + docs/src/man/creating_systems.md | 22 +++++++++++++++++ src/types/StateSpace.jl | 2 +- src/types/TransferFunction.jl | 37 +++++++++++++++++++++++++--- test/test_statespace.jl | 5 ++++ test/test_transferfunction.jl | 41 ++++++++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ea5c30fde..f487f12b8 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ using Pkg; Pkg.add("ControlSystems") - *Breaking*: Frequency-responses have changed data layout to `ny×nu×nω` from the previous `nω×ny×nu`. This is for performance reasons and to be consistent with time responses. This affects downstream functions `bode` and `nyquist` as well. - *Breaking*: `baltrunc` and `balreal` now return the diagonal of the Gramian as the second argument rather than the full matrix. - *Breaking*: The `pid` constructor no longer takes parameters as keyword arguments. `pid` has also gotten some new features, the new signature is `pid(P, I, D=0; form = :standard, Ts=nothing, Tf=nothing, state_space=false)`. This change affects downstream functions like `placePI, loopshapingPI, pidplots`. +- *Breaking*: The semantics of broadcasted multiplication between two systems was previously inconsistent between `StateSpace` and `TransferFunction`. The new behavior is documented under [Multiplying systems](https://juliacontrol.github.io/ControlSystems.jl/latest/man/creating_systems/#Multiplying-systems) in the documentation. ## Documentation diff --git a/docs/src/man/creating_systems.md b/docs/src/man/creating_systems.md index 79a184fbf..eac44a6a5 100644 --- a/docs/src/man/creating_systems.md +++ b/docs/src/man/creating_systems.md @@ -134,6 +134,28 @@ norm(Gmin - feedback(P), Inf) # No difference bodeplot([G, Gmin, feedback(P)]) # They are all identical ``` +## Multiplying systems +Two systems can be connected in series by multiplication +```@example MIMO +using ControlSystems +P1 = ss(-1,1,1,0) +P2 = ss(-2,1,1,0) +P2*P1 +``` +If the input dimension of `P2` does not match the output dimension of `P1`, an error is thrown. If one of the systems is SISO and the other is MIMO, broadcasted multiplication will expand the SISO system to match the input or output dimension of the MIMO system, e.g., +```@example MIMO +Pmimo = ssrand(2,2,1) +Psiso = ss(-2,1,1,0) +# Psiso * Pmimo # error +Psiso .* Pmimo ≈ [Psiso 0; 0 Psiso] * Pmimo # Broadcasted multiplication expands SISO into diagonal system +``` + +Broadcasted multiplication between a system and an array is only allowed for diagonal arrays +```@example MIMO +Psiso .* I(2) +``` + + ## MIMO systems and arrays of systems Concatenation of systems creates MIMO systems, which is different from an array of systems. For example ```@example MIMO diff --git a/src/types/StateSpace.jl b/src/types/StateSpace.jl index 9eb54d67d..39860e03a 100644 --- a/src/types/StateSpace.jl +++ b/src/types/StateSpace.jl @@ -464,7 +464,7 @@ function minreal(sys::T, tol=nothing; fast=false, atol=0.0, kwargs...) where T < atol = tol end Ar, Br, Cr = MatrixPencils.lsminreal(A,B,C; atol, fast, kwargs...) - T(Ar,Br,Cr,D, ntuple(i->getfield(sys, i+4), fieldcount(T)-4)...) + basetype(T)(Ar,Br,Cr,D, ntuple(i->getfield(sys, i+4), fieldcount(T)-4)...) end diff --git a/src/types/TransferFunction.jl b/src/types/TransferFunction.jl index 524bff3d2..c1303b5a6 100644 --- a/src/types/TransferFunction.jl +++ b/src/types/TransferFunction.jl @@ -169,15 +169,46 @@ function Base.Broadcast.broadcasted(::typeof(*), G1::TransferFunction, G2::Trans issiso(G1) || issiso(G2) || error("Only SISO transfer function can be broadcasted") # Note: G1*G2 = y <- G1 <- G2 <- u timeevol = common_timeevol(G1,G2) - matrix = G1.matrix .* G2.matrix + + if issiso(G1) && !issiso(G2) # Check !issiso(G2) to avoid calling fill if both are siso + G1 = append(G1 for i in 1:G2.ny) + elseif issiso(G2) + G2 = append(G2 for i in 1:G1.nu) + end + return G1 * G2 +end + +function Base.Broadcast.broadcasted(::typeof(*), G1::TransferFunction, M::AbstractArray) + issiso(G1) || error("Only SISO transfer function can be broadcasted") + LinearAlgebra.isdiag(M) || error("Broadcasting multiplication of an LTI system with an array is only supported for diagonal arrays. If you want the system to behave like a scalar and multiply each element of the array, wrap the system in a `Ref` to indicate this, i.e., `Ref(sys) .* array`.") + # Note: G1*G2 = y <- G1 <- G2 <- u + timeevol = G1.timeevol + matrix = G1.matrix .* M return TransferFunction(matrix, timeevol) end -function Base.Broadcast.broadcasted(::typeof(*), G1::TransferFunction, G2::AbstractArray) +function Base.Broadcast.broadcasted(::typeof(*), M::AbstractArray, G1::TransferFunction) issiso(G1) || error("Only SISO transfer function can be broadcasted") + LinearAlgebra.isdiag(M) || error("Broadcasting multiplication of an LTI system with an array is only supported for diagonal arrays. If you want the system to behave like a scalar and multiply each element of the array, wrap the system in a `Ref` to indicate this, i.e., `Ref(sys) .* array`.") # Note: G1*G2 = y <- G1 <- G2 <- u timeevol = G1.timeevol - matrix = G1.matrix .* G2 + matrix = M .* G1.matrix + return TransferFunction(matrix, timeevol) +end + +function Base.Broadcast.broadcasted(::typeof(*), G1r::Base.RefValue{<:TransferFunction}, M::AbstractArray) + G1 = G1r[] + issiso(G1) || error("Only SISO transfer function can be broadcasted") + timeevol = G1.timeevol + matrix = G1.matrix .* M + return TransferFunction(matrix, timeevol) +end + +function Base.Broadcast.broadcasted(::typeof(*), M::AbstractArray, G1r::Base.RefValue{<:TransferFunction}) + G1 = G1r[] + issiso(G1) || error("Only SISO transfer function can be broadcasted") + timeevol = G1.timeevol + matrix = M .* G1.matrix return TransferFunction(matrix, timeevol) end diff --git a/test/test_statespace.jl b/test/test_statespace.jl index a71c4208f..cb39cfaf3 100644 --- a/test/test_statespace.jl +++ b/test/test_statespace.jl @@ -89,6 +89,7 @@ @test minreal(C_111.*C_222_d - C_222_d.*C_111, atol=1e-3) == ss(0*I(2)) # scalar times MIMO @test C_111 .* C_222 == ss([-5 0 2 0; 0 -5 0 2; 0 0 -5 -3; 0 0 2 -9], [0 0; 0 0; 1 0; 0 2], [3 0 0 0; 0 3 0 0], 0) + @test_broken @inferred C_111 * C_221 @test_broken @inferred C_111 .* I(2) C_111_d = ssrand(1,1,2) @@ -124,6 +125,10 @@ M = randn(2,1) @test M .* Ref(C_111_d) == [C_111_d*M[1,1]; C_111_d*M[2,1]] + ## Test that tf behaves same as ss + @test minreal(tf(C_111 .* I(2))) == tf(C_111) .* I(2) + M = randn(2,2) + @test minreal(tf(minreal(Ref(C_111).*M))) ≈ Ref(tf(C_111)).*M # Test that multiplication/division is applied at correct input/output location @test (10*C_111).C == 10*C_111.C diff --git a/test/test_transferfunction.jl b/test/test_transferfunction.jl index 56b7487f9..1cc6b57f3 100644 --- a/test/test_transferfunction.jl +++ b/test/test_transferfunction.jl @@ -93,6 +93,47 @@ tf(vecarray(1, 2, [0], [0]), vecarray(1, 2, [1], [1]), 0.005) @test tf(1) .* C_222 == C_222 @test tf(1) .* I(2) == tf(I(2)) + +# Broadcasting +@test C_111 .* I(2) == I(2) .* C_111 +@test minreal(C_111.*C_222 - C_222.*C_111, 1e-3) == tf(ss(0*I(2))) # scalar times MIMO +@test C_111 .* C_222 == (C_111 .* I(2)) * C_222 + +@test_broken @inferred C_111 .* I(2) + +C_111_d = tf(ssrand(1,1,2)) +M = ones(2,2) + +@test_throws ErrorException C_111_d.*M # We do not allow broadcasting with non-diagonal matrices https://github.com/JuliaControl/ControlSystems.jl/issues/416 +# Unless we wrap the system in a Ref to indicate that we really want it to broadcast like a scalar + +@test Ref(C_111_d).*M == [C_111_d C_111_d; C_111_d C_111_d] + +M = ones(1,2) +@test Ref(C_111_d).*M == [C_111_d C_111_d] + +M = ones(2,1) +@test Ref(C_111_d).*M == [C_111_d; C_111_d] + +M = randn(2,2) +@test Ref(C_111_d).*M == [M[1,1]*C_111_d M[1,2]*C_111_d; M[2,1]*C_111_d M[2,2]*C_111_d] + +M = randn(1,2) +@test Ref(C_111_d).*M == [M[1]*C_111_d M[2]*C_111_d] + +M = randn(2,1) +@test Ref(C_111_d).*M == [M[1]*C_111_d; M[2]*C_111_d] + + +M = randn(2,2) +@test M .* Ref(C_111_d) ≈ [C_111_d*M[1,1] C_111_d*M[1,2]; C_111_d*M[2,1] C_111_d*M[2,2]] + +M = randn(1,2) +@test M .* Ref(C_111_d) ≈ [C_111_d*M[1,1] C_111_d*M[1,2]] + +M = randn(2,1) +@test M .* Ref(C_111_d) ≈ [C_111_d*M[1,1]; C_111_d*M[2,1]] + # Division @test 1/C_111 == tf([1,5], [1,2]) @test C_212/C_111 == tf(vecarray(2, 1, [1,7,13,15], [0,1,7,10]), From 89ccc81c8d4bf772ee207932d07ab9f09c95da55 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Mon, 11 Jul 2022 09:29:28 +0200 Subject: [PATCH 2/2] import LinAlg --- docs/src/man/creating_systems.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/man/creating_systems.md b/docs/src/man/creating_systems.md index eac44a6a5..924cfc6a6 100644 --- a/docs/src/man/creating_systems.md +++ b/docs/src/man/creating_systems.md @@ -152,6 +152,7 @@ Psiso .* Pmimo ≈ [Psiso 0; 0 Psiso] * Pmimo # Broadcasted multiplication expan Broadcasted multiplication between a system and an array is only allowed for diagonal arrays ```@example MIMO +using LinearAlgebra Psiso .* I(2) ```