Skip to content

Commit dad3579

Browse files
committed
automatically generate unique x names
1 parent 6cdf394 commit dad3579

File tree

2 files changed

+71
-50
lines changed

2 files changed

+71
-50
lines changed

src/named_systems2.jl

Lines changed: 65 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,49 @@
1-
macro check_unique(ex, s = string(ex), msg="")
2-
quote
3-
$(esc(__source__))
4-
vals = $(esc(ex))
5-
u = unique(vals)
6-
if length(u) != length(vals)
7-
rep = Dict{Symbol, Int}()
8-
for v in vals
9-
n = get(rep, v, 0) + 1
10-
rep[v] = n
11-
end
12-
rep = filter(((_,n),)-> n > 1, pairs(rep))
13-
repk = keys(rep)
14-
throw(ArgumentError($(s)*" names not unique. Repeated names: "*string(repk)*" "*$(esc(msg))))
1+
function check_unique(vals, s, msg=""; throw = true)
2+
u = unique(vals)
3+
if length(u) != length(vals)
4+
rep = Dict{Symbol, Int}()
5+
for v in vals
6+
n = get(rep, v, 0) + 1
7+
rep[v] = n
8+
end
9+
rep = filter(((_,n),)-> n > 1, pairs(rep))
10+
repk = keys(rep)
11+
if throw
12+
@__MODULE__().throw(ArgumentError(s*" names not unique. Repeated names: "*string(repk)*" "*msg))
13+
else
14+
return false
1515
end
1616
end
17+
return true
18+
end
19+
20+
function check_all_unique(s1, s2; throw=true)
21+
valx = check_unique([getproperty(s1, :x); getproperty(s2, :x)], "x"; throw)
22+
check_unique([getproperty(s1, :u); getproperty(s2, :u)], "u"; throw=true)
23+
check_unique([getproperty(s1, :y); getproperty(s2, :y)], "y"; throw=true)
24+
valx
1725
end
1826

19-
macro check_all_unique(s1, s2)
20-
quote
21-
vals = [getproperty($(esc(s1)), :x); getproperty($(esc(s2)), :x)]
22-
@check_unique vals "x"
23-
vals = [getproperty($(esc(s1)), :u); getproperty($(esc(s2)), :u)]
24-
@check_unique vals "u"
25-
vals = [getproperty($(esc(s1)), :y); getproperty($(esc(s2)), :y)]
26-
@check_unique vals "y"
27+
function generate_unique_x_names(systems...)
28+
x_names = [s.x for s in systems]
29+
uniq = check_unique(x_names, "x", throw = false)
30+
if uniq
31+
return x_names
32+
else
33+
# For each system, if it has a name, append the system name to the x names. If neither system has a name, append gensym names to both systems' x names.
34+
systemnames = [s.name for s in systems]
35+
x_names = if any(isempty(s.name) for s in systems) || length(unique(systemnames)) < length(systemnames)
36+
# Any name is empty or more than one system has the same name
37+
[gensym(string(x)) for x in x_names]
38+
else
39+
reduce(vcat, [[Symbol(string(s.name)*string(x)) for x in s.x] for s in systems])
40+
end
41+
@assert allunique(x_names)
42+
x_names
2743
end
2844
end
2945

46+
3047
import ControlSystemsBase as CS
3148
import ControlSystemsBase: nstates, blockdiag
3249

@@ -175,6 +192,9 @@ function named_ss(sys::AbstractStateSpace{T};
175192
name::String = "",
176193
unique = true,
177194
) where T
195+
if sys isa NamedStateSpace
196+
error("Cannot wrap a named statespace in a named statespace")
197+
end
178198
x = expand_symbol(x, sys.nx)
179199
u = expand_symbol(u, size(sys,2)) # size is used instead of sys.nu for ExtendedStateSpace
180200
y = expand_symbol(y, size(sys,1))
@@ -185,10 +205,10 @@ function named_ss(sys::AbstractStateSpace{T};
185205
length(y) == size(sys,1) ||
186206
throw(ArgumentError("Length of output names must match size(sys,1) ($(size(sys,1))), got length $(length(y))"))
187207

188-
@check_unique x "x"
189-
@check_unique y "y"
208+
check_unique(x, "x", "Cannot create a NamedStateSpace system with more than one variable with the same name")
209+
check_unique(y, "y", "Cannot create a NamedStateSpace system with more than one variable with the same name")
190210
if unique
191-
@check_unique u "u" "To allow connecting a single input signal to several inputs with the same name, pass `unique = false`."
211+
check_unique(u, "u", "To allow connecting a single input signal to several inputs with the same name, pass `unique = false`.")
192212
end
193213

194214
NamedStateSpace{T, typeof(sys)}(sys, x, u, y, name)
@@ -281,7 +301,7 @@ function Base.:+(s1::NamedStateSpace{T,S}, s2::NamedStateSpace{T,S}) where {T <:
281301
end
282302

283303
function Base.:*(s1::NamedStateSpace{T}, s2::NamedStateSpace{T}) where {T <: CS.TimeEvolution}
284-
@check_all_unique s1 s2
304+
x_names = generate_unique_x_names(s1, s2)
285305
if s1.u != s2.y
286306
connection_map = join(["$y -> $u" for (u,y) in zip(s1.u, s2.y) if u != y], '\n')
287307
@warn "Connected signals have different names\n $connection_map" maxlog=2
@@ -290,7 +310,7 @@ function Base.:*(s1::NamedStateSpace{T}, s2::NamedStateSpace{T}) where {T <: CS.
290310
S = typeof(sys)
291311
return NamedStateSpace{T,S}(
292312
sys,
293-
[s1.x; s2.x],
313+
x_names,
294314
s2.u,
295315
s1.y,
296316
"",
@@ -360,10 +380,9 @@ end
360380
##
361381

362382
function Base.hcat(systems::NamedStateSpace{T,S}...) where {T,S}
363-
x = reduce(vcat, getproperty.(systems, :x))
383+
x = generate_unique_x_names(systems...)
364384
u = reduce(vcat, getproperty.(systems, :u))
365-
@check_unique x
366-
@check_unique u
385+
check_unique(u, "u")
367386
return NamedStateSpace{T,S}(
368387
hcat(getproperty.(systems, :sys)...),
369388
x,
@@ -374,10 +393,9 @@ function Base.hcat(systems::NamedStateSpace{T,S}...) where {T,S}
374393
end
375394

376395
function Base.vcat(systems::NamedStateSpace{T,S}...) where {T,S}
377-
x = reduce(vcat, getproperty.(systems, :x))
396+
x = generate_unique_x_names(systems...)
378397
y = reduce(vcat, getproperty.(systems, :y))
379-
@check_unique x
380-
@check_unique y
398+
check_unique(y, "y")
381399
return NamedStateSpace{T,S}(
382400
vcat(getproperty.(systems, :sys)...),
383401
x,
@@ -467,6 +485,9 @@ All signal sets `w1,u1,z1,y1,u2,y2,w2,z2` have the same meaning as for the advan
467485
The added signal set `r` is used to optionally provide a new name for the input of the feedback loop.
468486
469487
To simplify creating complicated feedback interconnections, see `connect`.
488+
489+
If not all inputs and outputs are connected, the returned system may not be a minimal realization.
490+
Use `sminreal` (possibly also `minreal`) to simplify the system if only the input-output map is of interest.
470491
"""
471492
function ControlSystemsBase.feedback(s1::NamedStateSpace{T}, s2::NamedStateSpace{T};
472493
u1=:, w1=:,z1=:,y1=:,u2=:,y2=:,w2=[],z2=[], unique = true, kwargs...) where {T <: CS.TimeEvolution}
@@ -493,14 +514,13 @@ function ControlSystemsBase.feedback(s1::NamedStateSpace{T}, s2::NamedStateSpace
493514

494515
sys = feedback(s1.sys, s2.sys; W1, W2, U1, U2, Z1, Z2, Y1, Y2, kwargs...)
495516
fbname = gensym(:feedback)
496-
x1 = [s1.x; s2.x]
497-
@check_unique x1
517+
x1 = generate_unique_x_names(s1, s2) # [s1.x; s2.x]
498518
x1 = [Symbol(string(x1)*string(fbname)) for x1 in x1] # add unique name postfix
499519
@assert sys.nu == length(W1) + length(W2)
500520
@assert sys.ny == length(Z1) + length(Z2)
501521
@assert sys.nx == length(x1)
502522
nsys = NamedStateSpace{T,typeof(sys)}(sys, x1, [s1.u[W1]; s2.u[W2]], [s1.y[Z1]; s2.y[Z2]], "")
503-
sminreal(nsys)
523+
# sminreal(nsys)
504524
end
505525

506526
ControlSystemsBase.feedback(s1::NamedStateSpace{T}, s2::AbstractStateSpace{T}; kwargs...) where {T <: CS.TimeEvolution} = feedback(s1, named_ss(s2); kwargs...)
@@ -513,28 +533,28 @@ function connect(systems; u1::Vector{Symbol}, y1::Vector{Symbol}, external_input
513533
w1 = something(external_inputs, w1)
514534
w1 === nothing && error("The keyword argument `external_inputs` must be provided")
515535
if unique
516-
@check_unique u1 "Connected inputs not unique. If you want to connect several signals to the same input, use a summation node, e.g., named_ss(ss([1 1]), u=[:u1, :u2], y=:usum)"
517-
@check_unique full.u "system inputs" "To allow connecting a single input signal to several inputs with the same name, pass `unique = false`."
536+
check_unique(u1, "u1", "Connected inputs not unique. If you want to connect several signals to the same input, use a summation node, e.g., named_ss(ss([1 1]), u=[:u1, :u2], y=:usum)")
537+
check_unique(full.u, "system inputs" "To allow connecting a single input signal to several inputs with the same name, pass `unique = false`.")
518538
end
519539

520-
@check_unique full.y "system outputs"
540+
check_unique(full.y, "system outputs")
521541

522542
if verbose
523543
leftover_inputs = setdiff(full.u, [u1; w1])
524-
isempty(leftover_inputs) || @warn("The following inputs were unconnected $leftover_inputs, ignore this warning if you rely on prefix matching")
544+
isempty(leftover_inputs) || @warn("The following inputs were unconnected $leftover_inputs, ignore this warning if you rely on prefix matching. Turn off this warning by passing `verbose = false`.")
525545
leftover_outputs = setdiff(full.y, z1 == (:) ? y1 : [y1; z1])
526-
isempty(leftover_outputs) || @warn("The following outputs were unconnected $leftover_outputs, ignore this warning if you rely on prefix matching")
546+
isempty(leftover_outputs) || @warn("The following outputs were unconnected $leftover_outputs, ignore this warning if you rely on prefix matching. Turn off this warning by passing `verbose = false`.")
527547
end
528548

529549

530-
z2 = []
531-
w2 = []
550+
z2 = Symbol[]
551+
w2 = Symbol[]
532552

533553
# Connections
534554
y2 = (:)
535555
u2 = (:)
536-
537-
fb = named_ss(ss(I(length(y1)), full.timeevol))
556+
T = numeric_type(full)
557+
fb = named_ss(ss(one(T)*I(length(y1)), full.timeevol))
538558
feedback(full, fb; z1, z2, w1, w2, u1, u2, y1, y2, pos_feedback=true, unique, kwargs...)
539559
end
540560

test/test_named_systems2.jl

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using RobustAndOptimalControl, ControlSystemsBase
1+
using RobustAndOptimalControl, ControlSystemsBase, LinearAlgebra, Test, Plots
22
using RobustAndOptimalControl: @check_unique, @check_all_unique
33

44
@test :x^3 == expand_symbol(:x, 3) == [:x1, :x2, :x3]
@@ -102,7 +102,8 @@ s2 = named_ss(G2, x = [:z], u = [:u2], y=[:y2])
102102
G3 = s2/2.0
103103
@test ss(G3) == ss(s2)/2.0
104104

105-
@test_throws ArgumentError s1*s1
105+
s11 = s1*s1
106+
@test allunique(s11.x)
106107
end
107108

108109
@testset "Concatenation" begin
@@ -350,7 +351,7 @@ sys_connect2 = connect([sys_1, sys_2, split], [:out_z => :in_z, :in_y1=>:in_y1,
350351

351352
# Make gensym state names equal to assist equality testing below
352353
sys_connect2.x .= sys_connect.x
353-
@test sys_connect == sys_connect2
354+
@test sys_connect sys_connect2
354355

355356
# Discrete
356357
s1 = ssrand(1,2,2, Ts=1)
@@ -374,7 +375,7 @@ sys_connect2 = connect([sys_1, sys_2, split], [:out_z => :in_z, :in_y1=>:in_y1,
374375

375376
# Make gensym state names equal to assist equality testing below
376377
sys_connect2.x .= sys_connect.x
377-
@test sys_connect == sys_connect2
378+
@test sys_connect sys_connect2
378379

379380

380381
## Multiple identical names
@@ -406,7 +407,7 @@ sys_connect2 = connect([sys_1, sys_2, sys_3, split1, split2],
406407
unique = false
407408
)
408409
sys_connect2.x .= sys_connect.x
409-
@test sys_connect == sys_connect2
410+
@test sys_connect sys_connect2
410411

411412

412413
##

0 commit comments

Comments
 (0)