Skip to content

Commit 84796d2

Browse files
authored
Merge pull request #131 from baggepinnen/loopopenings
Allow loop openings in all sensitivity functions
2 parents baf499e + 0a91345 commit 84796d2

File tree

2 files changed

+123
-78
lines changed

2 files changed

+123
-78
lines changed

src/Blocks/analysis_points.jl

Lines changed: 92 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ end
9696
9797
Connect `output_connector` and `input_connector` with an [`AnalysisPoint`](@ref) inbetween.
9898
The incoming connection `output_connector` is expected to be of type [`RealOutput`](@ref), and vice versa.
99-
NOTE: The connection is assumed to be *causal*, meaning that
99+
100+
*PLEASE NOTE*: The connection is assumed to be *causal*, meaning that
100101
```
101102
connect(C.output, :plant_input, P.input)
102103
```
@@ -152,28 +153,44 @@ function Base.:(==)(ap1::AnalysisPoint, ap2::AnalysisPoint)
152153
return ap1.in == ap2.in && ap1.out == ap2.out # Name doesn't really matter if inputs and outputs are the same
153154
end
154155

155-
function get_sensitivity_function(sys, ap_name::Symbol; kwargs...)
156-
find = function (x, ns)
157-
x isa AnalysisPoint || return false
158-
if ns === nothing
159-
nameof(x) === ap_name
160-
else
161-
Symbol(ns, :_, nameof(x)) === ap_name
162-
end
156+
function namespaced_ap_match(ap_name, loop_openings)
157+
(x, ns) -> namespaced_ap_match(x, ns, ap_name, loop_openings)
158+
end # 🍛
159+
160+
"""
161+
namespaced_ap_match(x, ns, ap_name, loop_openings)
162+
163+
Returns true if `x` is an AnalysisPoint and matches either `ap_name` or any of the loop openings if namedspaced with `ns`.
164+
"""
165+
function namespaced_ap_match(x, ns, ap_name, loop_openings)
166+
x isa AnalysisPoint || return false
167+
if ns === nothing
168+
nameof(x) === ap_name || (loop_openings !== nothing && nameof(x) loop_openings)
169+
else
170+
xx = Symbol(ns, :_, nameof(x))
171+
xx === ap_name || (loop_openings !== nothing && xx loop_openings)
163172
end
173+
end
174+
175+
function get_sensitivity_function(sys, ap_name::Symbol; loop_openings = nothing, kwargs...)
176+
find = namespaced_ap_match(ap_name, loop_openings)
164177
t = get_iv(sys)
165178
@variables d(t) = 0 # Perturbation serving as input to sensitivity transfer function
166179
namespace = Ref{Union{Nothing, Symbol}}(nothing)
167180
apr = Ref{Union{Nothing, AnalysisPoint}}(nothing)
168181
replace = let d = d, namespace = namespace, apr = apr
169182
function (ap, ns)
170-
namespace[] = ns # Save the namespace to make it available for renamespace below
171-
apr[] = ap
172-
(ap.out.u ~ ap.in.u + d), d
183+
if namespaced_ap_match(ap, ns, ap_name, nothing)
184+
namespace[] = ns
185+
apr[] = ap
186+
(ap.out.u ~ ap.in.u + d), d
187+
else # loop opening
188+
[ap.out.u ~ 0], []
189+
end
173190
end
174191
end
175192
sys = expand_connections(sys, find, replace)
176-
(ap = apr[]) === nothing && error("Did not find analysis point $ap")
193+
(ap = apr[]) === nothing && error("Did not find analysis point $ap_name")
177194
u = ap.out.u
178195
if (ns = namespace[]) !== nothing
179196
d = ModelingToolkit.renamespace(ns, d)
@@ -182,28 +199,26 @@ function get_sensitivity_function(sys, ap_name::Symbol; kwargs...)
182199
ModelingToolkit.linearization_function(sys, [d], [u]; kwargs...)
183200
end
184201

185-
function get_comp_sensitivity_function(sys, ap_name::Symbol; kwargs...)
186-
find = function (x, ns)
187-
x isa AnalysisPoint || return false
188-
if ns === nothing
189-
nameof(x) === ap_name
190-
else
191-
Symbol(ns, :_, nameof(x)) === ap_name
192-
end
193-
end
202+
function get_comp_sensitivity_function(sys, ap_name::Symbol; loop_openings = nothing,
203+
kwargs...)
204+
find = namespaced_ap_match(ap_name, loop_openings)
194205
t = get_iv(sys)
195206
@variables d(t) = 0 # Perturbation serving as input to sensitivity transfer function
196207
namespace = Ref{Union{Nothing, Symbol}}(nothing)
197208
apr = Ref{Union{Nothing, AnalysisPoint}}(nothing)
198209
replace = let d = d, namespace = namespace, apr = apr
199210
function (ap, ns)
200-
namespace[] = ns # Save the namespace to make it available for renamespace below
201-
apr[] = ap
202-
(ap.out.u + d ~ ap.in.u), d
211+
if namespaced_ap_match(ap, ns, ap_name, nothing)
212+
namespace[] = ns
213+
apr[] = ap
214+
(ap.out.u + d ~ ap.in.u), d
215+
else # loop opening
216+
[ap.out.u ~ 0], []
217+
end
203218
end
204219
end
205220
sys = expand_connections(sys, find, replace)
206-
(ap = apr[]) === nothing && error("Did not find analysis point $ap")
221+
(ap = apr[]) === nothing && error("Did not find analysis point $ap_name")
207222
u = ap.in.u
208223
if (ns = namespace[]) !== nothing
209224
d = ModelingToolkit.renamespace(ns, d)
@@ -212,27 +227,24 @@ function get_comp_sensitivity_function(sys, ap_name::Symbol; kwargs...)
212227
ModelingToolkit.linearization_function(sys, [d], [u]; kwargs...)
213228
end
214229

215-
function get_looptransfer_function(sys, ap_name::Symbol; kwargs...)
216-
find = function (x, ns)
217-
x isa AnalysisPoint || return false
218-
if ns === nothing
219-
nameof(x) === ap_name
220-
else
221-
Symbol(ns, :_, nameof(x)) === ap_name
222-
end
223-
end
230+
function get_looptransfer_function(sys, ap_name::Symbol; loop_openings = nothing, kwargs...)
231+
find = namespaced_ap_match(ap_name, loop_openings)
224232
t = get_iv(sys)
225233
namespace = Ref{Union{Nothing, Symbol}}(nothing)
226234
apr = Ref{Union{Nothing, AnalysisPoint}}(nothing)
227235
replace = let namespace = namespace, apr = apr
228236
function (ap, ns)
229-
namespace[] = ns # Save the namespace to make it available for renamespace below
230-
apr[] = ap
231-
(0 ~ 0), nothing
237+
if namespaced_ap_match(ap, ns, ap_name, nothing)
238+
namespace[] = ns
239+
apr[] = ap
240+
(0 ~ 0), nothing
241+
else # loop opening
242+
[ap.out.u ~ 0], []
243+
end
232244
end
233245
end
234246
sys = expand_connections(sys, find, replace)
235-
(ap = apr[]) === nothing && error("Did not find analysis point $ap")
247+
(ap = apr[]) === nothing && error("Did not find analysis point $ap_name")
236248
u = ap.out.u
237249
y = ap.in.u
238250
if (ns = namespace[]) !== nothing
@@ -258,15 +270,8 @@ Open the loop at analysis point `ap` by breaking the connection through `ap`.
258270
259271
See also [`get_sensitivity`](@ref), [`get_comp_sensitivity`](@ref), [`get_looptransfer`](@ref).
260272
"""
261-
function open_loop(sys, ap_name::Symbol; kwargs...)
262-
find = function (x, ns)
263-
x isa AnalysisPoint || return false
264-
if ns === nothing
265-
nameof(x) === ap_name
266-
else
267-
Symbol(ns, :_, nameof(x)) === ap_name
268-
end
269-
end
273+
function open_loop(sys, ap_name::Symbol; ground_input = false, kwargs...)
274+
find = namespaced_ap_match(ap_name, nothing)
270275
t = get_iv(sys)
271276
@variables u(t)=0 [input = true]
272277
@variables y(t)=0 [output = true]
@@ -276,55 +281,61 @@ function open_loop(sys, ap_name::Symbol; kwargs...)
276281
function (ap, ns)
277282
namespace[] = ns # Save the namespace to make it available for renamespace below
278283
apr[] = ap
279-
[ap.out.u ~ u, ap.in.u ~ y], [u, y]
284+
if ground_input
285+
[ap.out.u ~ 0, ap.in.u ~ y], [y]
286+
else
287+
[ap.out.u ~ u, ap.in.u ~ y], [u, y]
288+
end
280289
end
281290
end
282-
if (ns = namespace[]) !== nothing
283-
y = ModelingToolkit.renamespace(ns, y)
284-
u = ModelingToolkit.renamespace(ns, u)
285-
end
286291
sys = expand_connections(sys, find, replace)
287-
(ap = apr[]) === nothing && error("Did not find analysis point $ap")
292+
(ap = apr[]) === nothing && error("Did not find analysis point $ap_name")
288293
sys
289294
end
290295

291296
function ModelingToolkit.linearization_function(sys::ModelingToolkit.AbstractSystem,
292297
input_name::Symbol, output_name::Symbol;
298+
loop_openings = nothing,
293299
kwargs...)
294-
find = function (x, ns)
295-
x isa AnalysisPoint || return false
296-
if ns === nothing
297-
nameof(x) (input_name, output_name)
298-
else
299-
Symbol(ns, :_, nameof(x)) (input_name, output_name)
300-
end
301-
end
302300
t = get_iv(sys)
303301
@variables u(t)=0 [input = true]
304302
@variables y(t)=0 [output = true]
305-
namespace = Ref{Union{Nothing, Symbol}}(nothing)
306-
apr = Ref{Union{Nothing, AnalysisPoint}}(nothing)
307-
replace = let u = u, y = y, namespace = namespace, apr = apr
303+
find_input = namespaced_ap_match(input_name, loop_openings)
304+
find_output = namespaced_ap_match(output_name, loop_openings)
305+
find = (x, ns) -> find_input(x, ns) || find_output(x, ns)
306+
307+
namespace_u = Ref{Union{Nothing, Symbol}}(nothing)
308+
namespace_y = Ref{Union{Nothing, Symbol}}(nothing)
309+
apr_u = Ref{Union{Nothing, AnalysisPoint}}(nothing)
310+
apr_y = Ref{Union{Nothing, AnalysisPoint}}(nothing)
311+
replace = let u = u, y = y, namespace_u = namespace_u, apr_u = apr_u,
312+
namespace_y = namespace_y, apr_y = apr_y
313+
308314
function (ap, ns)
309-
namespace[] = ns # Save the namespace to make it available for renamespace below
310-
apr[] = ap
311-
if nameof(ap) === input_name
315+
if namespaced_ap_match(ap, ns, input_name, nothing)
316+
namespace_u[] = ns # Save the namespace to make it available for renamespace below
317+
apr_u[] = ap
312318
[ap.out.u ~ ap.in.u + u], u
313319
#input.in.u ~ 0] # We only need to ground one of the ends, hence not including this equation
314-
elseif nameof(ap) === output_name
320+
elseif namespaced_ap_match(ap, ns, output_name, nothing)
321+
namespace_y[] = ns # Save the namespace to make it available for renamespace below
322+
apr_y[] = ap
315323
[ap.in.u ~ y
316324
ap.out.u ~ ap.in.u], y
317-
else
318-
error("This should never happen")
325+
else # loop opening
326+
[ap.out.u ~ 0], []
319327
end
320328
end
321329
end
322330
sys = expand_connections(sys, find, replace)
323-
(ap = apr[]) === nothing && error("Did not find analysis point $ap")
324-
if (ns = namespace[]) !== nothing
325-
y = ModelingToolkit.renamespace(ns, y)
331+
(ap = apr_u[]) === nothing && error("Did not find analysis point $input_name")
332+
(ap = apr_y[]) === nothing && error("Did not find analysis point $output_name")
333+
if (ns = namespace_u[]) !== nothing
326334
u = ModelingToolkit.renamespace(ns, u)
327335
end
336+
if (ns = namespace_y[]) !== nothing
337+
y = ModelingToolkit.renamespace(ns, y)
338+
end
328339
ModelingToolkit.linearization_function(sys, [u], [y]; kwargs...)
329340
end
330341

@@ -342,8 +353,9 @@ end
342353

343354
# Methods above are implemented in terms of linearization_function, the method below creates wrappers for linearize
344355
for f in [:get_sensitivity, :get_comp_sensitivity, :get_looptransfer]
345-
@eval function $f(sys, ap::Symbol, args...; kwargs...)
346-
lin_fun, ssys = $(Symbol(string(f) * "_function"))(sys, ap, args...; kwargs...)
356+
@eval function $f(sys, ap::Symbol, args...; loop_openings = nothing, kwargs...)
357+
lin_fun, ssys = $(Symbol(string(f) * "_function"))(sys, ap, args...; loop_openings,
358+
kwargs...)
347359
ModelingToolkit.linearize(ssys, lin_fun; kwargs...), ssys
348360
end
349361
end
@@ -353,8 +365,10 @@ end
353365
354366
Linearize a system between two analysis points. To get a loop-transfer function, see [`get_looptransfer`](@ref)
355367
"""
356-
function ModelingToolkit.linearize(sys, input_name::Symbol, output_name::Symbol; kwargs...)
357-
lin_fun, ssys = linearization_function(sys, input_name, output_name; kwargs...)
368+
function ModelingToolkit.linearize(sys, input_name::Symbol, output_name::Symbol;
369+
loop_openings = nothing, kwargs...)
370+
lin_fun, ssys = linearization_function(sys, input_name, output_name; loop_openings,
371+
kwargs...)
358372
ModelingToolkit.linearize(ssys, lin_fun; kwargs...), ssys
359373
end
360374

test/Blocks/test_analysis_points.jl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ using ModelingToolkit
33
using ModelingToolkitStandardLibrary.Blocks
44
using OrdinaryDiffEq
55
using ModelingToolkit: get_eqs, vars, @set!, get_iv
6+
using ControlSystemsBase
67

78
@named P = FirstOrder(k = 1, T = 1)
89
@named C = Gain(-1)
@@ -224,3 +225,33 @@ matrices, ssys = get_sensitivity(closed_loop, :u)
224225
Si = ss(matrices...)
225226

226227
@test tf(So) tf(Si)
228+
229+
## A simple multi-level system with loop openings
230+
@parameters t
231+
@named P_inner = FirstOrder(k = 1, T = 1)
232+
@named feedback = Feedback()
233+
@named ref = Step()
234+
@named sys_inner = ODESystem([connect(P_inner.output, :y, feedback.input2)
235+
connect(feedback.output, :u, P_inner.input)
236+
connect(ref.output, :r, feedback.input1)], t,
237+
systems = [P_inner, feedback, ref])
238+
239+
Sinner = sminreal(ss(get_sensitivity(sys_inner, :u)[1]...))
240+
241+
@named sys_inner = ODESystem([connect(P_inner.output, :y, feedback.input2)
242+
connect(feedback.output, :u, P_inner.input)], t,
243+
systems = [P_inner, feedback])
244+
245+
@named P_outer = FirstOrder(k = rand(), T = rand())
246+
247+
@named sys_outer = ODESystem([connect(sys_inner.P_inner.output, :y2, P_outer.input)
248+
connect(P_outer.output, :u2, sys_inner.feedback.input1)], t,
249+
systems = [P_outer, sys_inner])
250+
251+
Souter = sminreal(ss(get_sensitivity(sys_outer, :sys_inner_u)[1]...))
252+
253+
Sinner2 = sminreal(ss(get_sensitivity(sys_outer, :sys_inner_u, loop_openings = [:y2])[1]...))
254+
255+
@test Sinner.nx == 1
256+
@test Sinner == Sinner2
257+
@test Souter.nx == 2

0 commit comments

Comments
 (0)