Skip to content

Commit 32654d6

Browse files
authored
Merge pull request #2361 from SciML/baggepinnen-patch-4
Handle dummy derivatives in `linearization_function`
2 parents ddcace4 + df7c565 commit 32654d6

File tree

3 files changed

+167
-1
lines changed

3 files changed

+167
-1
lines changed

src/systems/abstractsystem.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1303,7 +1303,7 @@ function linearization_function(sys::AbstractSystem, inputs,
13031303
op = merge(defs, op)
13041304
end
13051305
sys = ssys
1306-
x0 = merge(defaults(sys), op)
1306+
x0 = merge(defaults(sys), Dict(missing_variable_defaults(sys)), op)
13071307
u0, p, _ = get_u0_p(sys, x0, p; use_union = false, tofloat = true)
13081308
p, split_idxs = split_parameters_by_type(p)
13091309
ps = parameters(sys)

test/inversemodel.jl

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
using ModelingToolkit
2+
using ModelingToolkitStandardLibrary
3+
using ModelingToolkitStandardLibrary.Blocks
4+
using OrdinaryDiffEq
5+
using Test
6+
using ControlSystemsMTK: tf, ss, get_named_sensitivity, get_named_comp_sensitivity
7+
8+
# ==============================================================================
9+
## Mixing tank
10+
# This tests a common workflow in control engineering, the use of an inverse-based
11+
# feedforward model. Such a model differentiates "inputs", exercising the dummy-derivative functionality of ModelingToolkit. We also test linearization and computation of sensitivity functions
12+
# for such models.
13+
# ==============================================================================
14+
15+
connect = ModelingToolkit.connect;
16+
@parameters t;
17+
D = Differential(t);
18+
rc = 0.25 # Reference concentration
19+
20+
@mtkmodel MixingTank begin
21+
@parameters begin
22+
c0 = 0.8, [description = "Nominal concentration"]
23+
T0 = 308.5, [description = "Nominal temperature"]
24+
a1 = 0.2674
25+
a21 = 1.815
26+
a22 = 0.4682
27+
b = 1.5476
28+
k0 = 1.05e14
29+
ϵ = 34.2894
30+
end
31+
32+
@variables begin
33+
gamma(t), [description = "Reaction speed"]
34+
xc(t) = c0, [description = "Concentration"]
35+
xT(t) = T0, [description = "Temperature"]
36+
xT_c(t) = T0, [description = "Cooling temperature"]
37+
end
38+
39+
@components begin
40+
T_c = RealInput()
41+
c = RealOutput()
42+
T = RealOutput()
43+
end
44+
45+
begin
46+
τ0 = 60
47+
wk0 = k0 / c0
48+
= ϵ * T0
49+
wa11 = a1 / τ0
50+
wa12 = c0 / τ0
51+
wa13 = c0 * a1 / τ0
52+
wa21 = a21 / τ0
53+
wa22 = a22 * T0 / τ0
54+
wa23 = T0 * (a21 - b) / τ0
55+
wb = b / τ0
56+
end
57+
@equations begin
58+
gamma ~ xc * wk0 * exp(-/ xT)
59+
D(xc) ~ -wa11 * xc - wa12 * gamma + wa13
60+
D(xT) ~ -wa21 * xT + wa22 * gamma + wa23 + wb * xT_c
61+
62+
xc ~ c.u
63+
xT ~ T.u
64+
xT_c ~ T_c.u
65+
end
66+
end
67+
68+
begin
69+
Ftf = tf(1, [(100), 1])^3
70+
Fss = ss(Ftf)
71+
72+
"Compute initial state that yields y0 as output"
73+
function init_filter(y0)
74+
(; A, B, C, D) = Fss
75+
Fx0 = -A \ B * y0
76+
@assert C * Fx0[y0] "C*Fx0*y0 ≈ y0 failed, got $(C*Fx0*y0)$(y0)]"
77+
Fx0
78+
end
79+
80+
# Create an MTK-compatible constructor
81+
RefFilter(; y0, name) = ODESystem(Fss; name, x0 = init_filter(y0))
82+
end
83+
@mtkmodel InverseControlledTank begin
84+
begin
85+
c0 = 0.8 # "Nominal concentration
86+
T0 = 308.5 # "Nominal temperature
87+
x10 = 0.42
88+
x20 = 0.01
89+
u0 = -0.0224
90+
91+
c_start = c0 * (1 - x10) # Initial concentration
92+
T_start = T0 * (1 + x20) # Initial temperature
93+
c_high_start = c0 * (1 - 0.72) # Reference concentration
94+
T_c_start = T0 * (1 + u0) # Initial cooling temperature
95+
end
96+
@components begin
97+
ref = Constant(k = 0.25) # Concentration reference
98+
ff_gain = Gain(k = 1) # To allow turning ff off
99+
controller = PI(gainPI.k = 10, T = 500)
100+
tank = MixingTank(xc = c_start, xT = T_start, c0 = c0, T0 = T0)
101+
inverse_tank = MixingTank(xc = c_start, xT = T_start, c0 = c0, T0 = T0)
102+
feedback = Feedback()
103+
add = Add()
104+
filter = RefFilter(y0 = c_start) # Initialize filter states to the initial concentration
105+
noise_filter = FirstOrder(k = 1, T = 1, x = T_start)
106+
# limiter = Gain(k=1)
107+
limiter = Limiter(y_max = 370, y_min = 250) # Saturate the control input
108+
end
109+
@equations begin
110+
connect(ref.output, :r, filter.input)
111+
connect(filter.output, inverse_tank.c)
112+
113+
connect(inverse_tank.T_c, ff_gain.input)
114+
connect(ff_gain.output, :uff, limiter.input)
115+
connect(limiter.output, add.input1)
116+
117+
connect(controller.ctr_output, :u, add.input2)
118+
119+
#connect(add.output, :u_tot, limiter.input)
120+
#connect(limiter.output, :v, tank.T_c)
121+
122+
connect(add.output, :u_tot, tank.T_c)
123+
124+
connect(inverse_tank.T, feedback.input1)
125+
126+
connect(tank.T, :y, noise_filter.input)
127+
128+
connect(noise_filter.output, feedback.input2)
129+
connect(feedback.output, :e, controller.err_input)
130+
end
131+
end;
132+
@named model = InverseControlledTank()
133+
ssys = structural_simplify(model)
134+
cm = complete(model)
135+
136+
op = Dict(D(cm.inverse_tank.xT) => 1,
137+
cm.tank.xc => 0.65)
138+
tspan = (0.0, 1000.0)
139+
prob = ODEProblem(ssys, op, tspan)
140+
sol = solve(prob, Rodas5P())
141+
142+
@test SciMLBase.successful_retcode(sol)
143+
144+
# plot(sol, idxs=[model.tank.xc, model.tank.xT, model.controller.ctr_output.u], layout=3, sp=[1 2 3])
145+
# hline!([prob[cm.ref.k]], label="ref", sp=1)
146+
147+
@test sol(tspan[2], idxs = cm.tank.xc)prob[cm.ref.k] atol=1e-2 # Test that the inverse model led to the correct reference
148+
149+
Sf, simplified_sys = Blocks.get_sensitivity_function(model, :y) # This should work without providing an operating opint containing a dummy derivative
150+
x, p = ModelingToolkit.get_u0_p(simplified_sys, op)
151+
matrices1 = Sf(x, p, 0)
152+
matrices2, _ = Blocks.get_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API
153+
@test matrices1.f_x matrices2.A[1:7, 1:7]
154+
nsys = get_named_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API
155+
@test matrices2.A nsys.A
156+
157+
# Test the same thing for comp sensitivities
158+
159+
Sf, simplified_sys = Blocks.get_comp_sensitivity_function(model, :y) # This should work without providing an operating opint containing a dummy derivative
160+
x, p = ModelingToolkit.get_u0_p(simplified_sys, op)
161+
matrices1 = Sf(x, p, 0)
162+
matrices2, _ = Blocks.get_comp_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API
163+
@test matrices1.f_x matrices2.A[1:7, 1:7]
164+
nsys = get_named_comp_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API
165+
@test matrices2.A nsys.A

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ end
5555
@safetestset "OptimizationSystem Test" include("optimizationsystem.jl")
5656
@safetestset "FuncAffect Test" include("funcaffect.jl")
5757
@safetestset "Constants Test" include("constants.jl")
58+
@safetestset "Inverse Models Test" include("inversemodel.jl")
5859
# Reference tests go Last
5960
if VERSION >= v"1.9"
6061
@safetestset "Latexify recipes Test" include("latexify.jl")

0 commit comments

Comments
 (0)