Skip to content

Commit 4a7b354

Browse files
committed
handle systems with subsystems
depends on SciML/ModelingToolkit.jl#1827
1 parent ae1910a commit 4a7b354

File tree

2 files changed

+99
-14
lines changed

2 files changed

+99
-14
lines changed

src/Blocks/analysis_points.jl

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ end
108108
Find and return the analysis point in `sys` with the specified `name`. If no matching [`AnalysisPoint`](@ref) is found, `nothing` is returned.
109109
"""
110110
function find_analysis_point(sys, name)
111-
for eq in equations(sys)
111+
sys = ModelingToolkit.flatten(sys)
112+
eqs = equations(sys)
113+
for eq in eqs
112114
eq.rhs isa AnalysisPoint && eq.rhs.name == name && (return eq.rhs)
113115
end
114116
nothing
@@ -120,6 +122,7 @@ end
120122
Replace analysis points with the identity connection connect(ap.in, ap.out). This is called before a system containing analysis points is simulated, in which case analysis points have no effect.
121123
"""
122124
function expand_analysis_points(sys)
125+
sys = ModelingToolkit.flatten(sys) # TODO: this does not namespace variables in connect statements properly https://github.com/SciML/ModelingToolkit.jl/issues/1826
123126
new_eqs = map(get_eqs(sys)) do eq
124127
eq.rhs isa AnalysisPoint || (return eq)
125128
ap = eq.rhs
@@ -129,6 +132,17 @@ function expand_analysis_points(sys)
129132
sys
130133
end
131134

135+
function ModelingToolkit.namespace_expr(ap::AnalysisPoint, sys, n = nameof(sys)) where {T}
136+
in = ModelingToolkit.renamespace(n, ap.in)
137+
out = ModelingToolkit.renamespace(n, ap.out)
138+
name = Symbol(n, :_, ap.name)
139+
AnalysisPoint(in, out, name)
140+
end
141+
142+
function Base.:(==)(ap1::AnalysisPoint, ap2::AnalysisPoint)
143+
return ap1.in == ap2.in && ap1.out == ap2.out # Name doesn't really matter if inputs and outputs are the same
144+
end
145+
132146
"""
133147
get_sensitivity(sys, ap::AnalysisPoint; kwargs)
134148
get_sensitivity(sys, ap_name::Symbol; kwargs)
@@ -141,15 +155,20 @@ Compute the sensitivity function in analysis point `ap`. The sensitivity functio
141155
See also [`get_comp_sensitivity`](@ref), [`get_looptransfer`](@ref).
142156
"""
143157
function get_sensitivity(sys, ap::AnalysisPoint; kwargs...)
158+
sys = ModelingToolkit.flatten(sys) # To get namespacing right
144159
t = get_iv(sys)
145160
@variables d(t) = 0 # Perturbation serving as input to sensivity transfer function
146-
new_eqs = map(get_eqs(sys)) do eq
161+
found = false
162+
new_eqs = map(equations(sys)) do eq
147163
eq.rhs == ap || (return eq)
148-
ap.out.u ~ ap.in.u + d # This assumes that the connector as an internal vaiable named u
164+
found = true
165+
ap.out.u ~ ap.in.u + d # This assumes that the connector has an internal vaiable named u
149166
end
167+
found || error("Did not find analysis point $ap")
150168
@set! sys.eqs = new_eqs
151169
@set! sys.states = [states(sys); d]
152-
sys = expand_analysis_points(sys)
170+
@set! sys.defaults = merge(ModelingToolkit.defaults(sys), Dict(d => 0))
171+
sys = expand_analysis_points(sys) # Any remaining analysis points are removed by this
153172
ModelingToolkit.linearize(sys, [d], [ap.out.u]; kwargs...)
154173
end
155174

@@ -165,15 +184,21 @@ Compute the complementary sensitivity function in analysis point `ap`. The compl
165184
See also [`get_sensitivity`](@ref), [`get_looptransfer`](@ref).
166185
"""
167186
function get_comp_sensitivity(sys, ap::AnalysisPoint; kwargs...)
187+
sys = ModelingToolkit.flatten(sys) # To get namespacing right
168188
t = get_iv(sys)
169189
@variables d(t) = 0 # Perturbation serving as input to sensivity transfer function
170-
new_eqs = map(get_eqs(sys)) do eq
190+
found = false
191+
new_eqs = map(equations(sys)) do eq
171192
eq.rhs == ap || (return eq)
172-
ap.out.u + d ~ ap.in.u # This assumes that the connector as an internal vaiable named u
193+
found = true
194+
ap.out.u + d ~ ap.in.u # This assumes that the connector has an internal vaiable named u
173195
end
196+
found || error("Did not find analysis point $ap")
174197
@set! sys.eqs = new_eqs
175198
@set! sys.states = [states(sys); d]
176-
sys = expand_analysis_points(sys)
199+
@set! sys.defaults = merge(ModelingToolkit.defaults(sys), Dict(d => 0))
200+
201+
sys = expand_analysis_points(sys) # Any remaining analysis points are removed by this
177202
ModelingToolkit.linearize(sys, [d], [ap.in.u]; kwargs...)
178203
end
179204

@@ -189,13 +214,17 @@ Compute the (linearized) loop-transfer function in analysis point `ap`, from `ap
189214
See also [`get_sensitivity`](@ref), [`get_comp_sensitivity`](@ref), [`open_loop`](@ref).
190215
"""
191216
function get_looptransfer(sys, ap::AnalysisPoint; kwargs...)
217+
sys = ModelingToolkit.flatten(sys) # To get namespacing right
192218
t = get_iv(sys)
193-
new_eqs = map(get_eqs(sys)) do eq
219+
found = false
220+
new_eqs = map(equations(sys)) do eq
194221
eq.rhs == ap || (return eq)
195-
0 ~ 0 # we just want to open the connection
222+
found = true
223+
0 ~ 0 # This assumes that the connector has an internal vaiable named u
196224
end
225+
found || error("Did not find analysis point $ap")
197226
@set! sys.eqs = new_eqs
198-
sys = expand_analysis_points(sys)
227+
sys = expand_analysis_points(sys) # Any remaining analysis points are removed by this
199228
ModelingToolkit.linearize(sys, [ap.out.u], [ap.in.u]; kwargs...)
200229
end
201230

@@ -205,25 +234,30 @@ end
205234
206235
Open the loop at analysis point `ap` by breaking the connection through `ap`.
207236
208-
`open_sys` will have `ap.out` as input and `ap.in` as output.
237+
`open_sys` will have `u ~ ap.out` as input and `y ~ ap.in` as output.
209238
210239
# Arguments:
211240
- `kwargs`: Are sent to `ModelingToolkit.linearize`
212241
213242
See also [`get_sensitivity`](@ref), [`get_comp_sensitivity`](@ref), [`get_looptransfer`](@ref).
214243
"""
215244
function open_loop(sys, ap::AnalysisPoint; kwargs...)
245+
sys = ModelingToolkit.flatten(sys) # To get namespacing right
216246
t = get_iv(sys)
217247
@variables u(t)=0 [input = true]
218248
@variables y(t)=0 [output = true]
219-
new_eqs = map(get_eqs(sys)) do eq
249+
found = false
250+
new_eqs = map(equations(sys)) do eq
220251
eq.rhs == ap || (return [eq])
252+
found = true
221253
[ap.out.u ~ u
222254
ap.in.u ~ y]
223255
end
256+
found || error("Did not find analysis point $ap")
224257
new_eqs = reduce(vcat, new_eqs)
225258
@set! sys.eqs = new_eqs
226259
@set! sys.states = [states(sys); u; y]
260+
@set! sys.defaults = merge(ModelingToolkit.defaults(sys), Dict(u => 0, y => 0))
227261
sys
228262
end
229263

@@ -235,10 +269,11 @@ All parts of the model that do not appear between `input` and `output` will be n
235269
"""
236270
function ModelingToolkit.linearize(sys, input::AnalysisPoint, output::AnalysisPoint;
237271
kwargs...)
272+
sys = ModelingToolkit.flatten(sys) # To get namespacing right
238273
t = get_iv(sys)
239274
@variables u(t)=0 [input = true]
240275
@variables y(t)=0 [output = true]
241-
new_eqs = map(get_eqs(sys)) do eq
276+
new_eqs = map(equations(sys)) do eq
242277
if eq.rhs == input
243278
[input.out.u ~ u]
244279
#input.in.u ~ 0] # We only need to ground one of the ends, hence not including this equation
@@ -252,20 +287,25 @@ function ModelingToolkit.linearize(sys, input::AnalysisPoint, output::AnalysisPo
252287
new_eqs = reduce(vcat, new_eqs)
253288
@set! sys.eqs = new_eqs
254289
@set! sys.states = [states(sys); u; y]
290+
@set! sys.defaults = merge(ModelingToolkit.defaults(sys), Dict(u => 0, y => 0))
255291
sys = expand_analysis_points(sys)
256292
ModelingToolkit.linearize(sys, [u], [y]; kwargs...)
257293
end
258294

259295
# Add a method to get_sensitivity that accepts the name of an AnalysisPoint
260296
for f in [:get_sensitivity, :get_comp_sensitivity, :get_looptransfer, :open_loop]
261297
@eval function $f(sys, ap_name::Symbol, args...; kwargs...)
262-
$f(sys, find_analysis_point(sys, ap_name), args...; kwargs...)
298+
ap = find_analysis_point(sys, ap_name)
299+
ap === nothing && error("Failed to find an analysis point named $ap_name")
300+
$f(sys, ap, args...; kwargs...)
263301
end
264302
end
265303

266304
function ModelingToolkit.linearize(sys, input_name::Symbol, output_name::Symbol;
267305
kwargs...)
268306
input = find_analysis_point(sys, input_name)
307+
input === nothing && error("Failed to find an analysis point named $input_name")
269308
output = find_analysis_point(sys, output_name)
309+
output === nothing && error("Failed to find an analysis point named $output_name")
270310
ModelingToolkit.linearize(sys, input, output; kwargs...)
271311
end

test/Blocks/test_analysis_points.jl

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,48 @@ matrices, _ = linearize(sys, :plant_input, :plant_output)
9393
@test matrices.A[] == -1
9494
@test matrices.B[] * matrices.C[] == 1 # both positive
9595
@test matrices.D[] == 0
96+
97+
98+
## Test with subsystems
99+
100+
@named P = FirstOrder(k = 1, T = 1)
101+
@named C = Gain(1)
102+
@named add = Blocks.Add(k2=-1)
103+
t = ModelingToolkit.get_iv(P)
104+
105+
eqs = [connect(P.output, :plant_output, add.input2)
106+
connect(add.output, C.input)
107+
connect(C.output, :plant_input, P.input)]
108+
109+
# eqs = [connect(P.output, add.input2)
110+
# connect(add.output, C.input)
111+
# connect(C.output, P.input)]
112+
113+
sys_inner = ODESystem(eqs, t, systems = [P, C, add], name = :inner)
114+
115+
@named r = Constant(k = 1)
116+
@named F = FirstOrder(k = 1, T = 3)
117+
118+
eqs = [connect(r.output, F.input)
119+
connect(F.output, sys_inner.add.input1)]
120+
sys_outer = ODESystem(eqs, t, systems = [F, sys_inner, r], name = :outer)
121+
122+
123+
# test first that the structural_simplify ∘ expand_analysis_points works correctly
124+
ssys = structural_simplify(expand_analysis_points(sys_outer))
125+
# ssys = structural_simplify((sys_outer))
126+
prob = ODEProblem(ssys, [P.x => 1], (0, 10))
127+
# sol = solve(prob, Rodas5())
128+
# plot(sol)
129+
130+
matrices, _ = get_sensitivity(sys_outer, :inner_plant_input)
131+
132+
using ControlSystemsBase # This is required to simplify the results to test against known solution
133+
lsys = sminreal(ss(matrices...))
134+
@test lsys.A[] == -2
135+
@test lsys.B[] * lsys.C[] == -1 # either one negative
136+
@test lsys.D[] == 1
137+
138+
matrices_So, _ = get_sensitivity(sys_outer, :inner_plant_output)
139+
lsyso = sminreal(ss(matrices_So...))
140+
@test lsys == lsyso || lsys == -1*lsyso*(-1) # Output and input sensitivites are equal for SISO systems

0 commit comments

Comments
 (0)