Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make tf broadcast as ss #714

Merged
merged 2 commits into from
Jul 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions docs/src/man/creating_systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,29 @@ 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
using LinearAlgebra
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
Expand Down
2 changes: 1 addition & 1 deletion src/types/StateSpace.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
37 changes: 34 additions & 3 deletions src/types/TransferFunction.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions test/test_statespace.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions test/test_transferfunction.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand Down