-
Notifications
You must be signed in to change notification settings - Fork 0
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
Better integration with ModellingToolkit #106
Comments
Please consider working through and contributing this example @ufechner7 |
FYI, I'm working on some (final) breaking change for the v1.0.0 release that should simplify these steps. I will change the order of the input arguments to match the order of MTK and I will officially support a time |
A problem I see with including an MTK example is that it is rather involved to cover all corner cases that MTK exposes. Committing to an interface to MTK is a substantial amount of nontrivial work, and we do not want to give users false indications of a well supported interface. |
I don't plan on making an official interface to MTK, but a section in the manual that exposes the steps for that would be clearly useful (with a disclaimer that it's not an official interface). I personally don't have much experience with MTK so it would not be easy task for me (and my little experience with MTK was rather bad in term of clear error messages) Anyway, there is already JuliaSimControl that is tightly integrated with MTK by design. No need to overlap on this specific feature. |
I can try making a working example. And yes, JuliaSimControl is tightly integrated with MTK, but it is not open source. So I think more people would be interested in documentation for using MTK with ModelPredictiveControl.jl. |
@franckgaga do you know how soon you will make those breaking changes? |
You can use this example as a guide https://baggepinnen.github.io/ControlSystemIdentification.jl/dev/nonlinear/#Identifying-parameters-in-a-ModelingToolkit-model |
I will work on these feature this week and in the next week. I would say in max 2 weeks these changes will be released. |
The following example works until the last 4 lines. In these last lines, ForwardDiff tries to run f! with ForwardDiff.Dual numbers, but this does not work with the generated f! function. Any ideas on how to fix this? using ModelPredictiveControl
using ModelingToolkit
using ModelingToolkit: D_nounits as D, t_nounits as t
@mtkmodel Pendulum begin
@parameters begin
g = 9.8
L = 0.4
K = 1.2
m = 0.3
end
@variables begin
θ(t) # state
ω(t) # state
τ(t) # input
y(t) # output
end
@equations begin
D(θ) ~ ω
D(ω) ~ -g/L*sin(θ) - K/m*ω + τ/m/L^2
y ~ θ * 180 / π
end
end
function generate_f_h(model, par)
@named mtkmodel = model()
mtkmodel = complete(mtkmodel)
inputs = [mtkmodel.τ]
outputs = [mtkmodel.y]
(f_, f_ip), dvs, psym, io_sys = ModelingToolkit.generate_control_function(mtkmodel, inputs, split=false; outputs=outputs)
h_ = ModelingToolkit.build_explicit_observed_function(io_sys, outputs; inputs = inputs)
function f!(dx, x, u, _)
@show dx
dx .= f_(x, u, par, 1)
end
function h!(y, x, _)
y .= h_(x, 1, par, 1)
end
return f!, h!
end
const par = (9.8, 0.4, 1.2, 0.3)
f!, h! = generate_f_h(Pendulum, par)
nu, nx, ny, Ts = 1, 2, 1, 0.1
vu, vx, vy = ["\$τ\$ (Nm)"], ["\$θ\$ (rad)", "\$ω\$ (rad/s)"], ["\$θ\$ (°)"]
model = setname!(NonLinModel(f!, h!, Ts, nu, nx, ny); u=vu, x=vx, y=vy)
using Plots
u = [0.5]
N = 35
res = sim!(model, N, u)
display(plot(res, plotu=false))
α=0.01; σQ=[0.1, 1.0]; σR=[5.0]; nint_u=[1]; σQint_u=[0.1]
estim = UnscentedKalmanFilter(model; α, σQ, σR, nint_u, σQint_u)
const par_plant = (par[1], par[2], 1.25*par[3], par[4])
f_plant, h_plant = generate_f_h(Pendulum, par_plant)
plant = setname!(NonLinModel(f_plant, h_plant, Ts, nu, nx, ny); u=vu, x=vx, y=vy)
@time res = sim!(estim, N, [0.5], plant=plant, y_noise=[0.5])
display(plot(res, plotu=false, plotxwithx̂=true))
Hp, Hc, Mwt, Nwt = 20, 2, [0.5], [2.5]
nmpc = NonLinMPC(estim; Hp, Hc, Mwt, Nwt, Cwt=Inf)
umin, umax = [-1.5], [+1.5]
nmpc = setconstraint!(nmpc; umin, umax)
res_ry = sim!(nmpc, N, [180.0], plant=plant, x_0=[0, 0], x̂_0=[0, 0, 0])
display(plot(res_ry))
res_yd = sim!(nmpc, N, [180.0], plant=plant, x_0=[π, 0], x̂_0=[π, 0, 0], y_step=[10])
display(plot(res_yd))
|
In function f!(dx, x, u, _)
@show dx
dx .= f_(x, u, par, 1)
end you could use This works function generate_f_h(model, par)
@named mtkmodel = model()
mtkmodel = complete(mtkmodel)
inputs = [mtkmodel.τ]
outputs = [mtkmodel.y]
(f_, f_ip), dvs, psym, io_sys = ModelingToolkit.generate_control_function(mtkmodel, inputs, split=false; outputs=outputs)
any(ModelingToolkit.is_alg_equation, equations(io_sys)) && error("Systems with algebraic equations are not supported")
h_ = ModelingToolkit.build_explicit_observed_function(io_sys, outputs; inputs = inputs)
function f!(dx, x, u, _)
f_ip(dx, x, u, par, 1)
dx
end
function h!(y, x, _)
y .= h_(x, 1, par, 1)
end
return f!, h!
end |
Another note, MTK does not guarantee that the state realization remains constant between versions (even patch versions are allowed to change this), the way you assign any(ModelingToolkit.is_alg_equation, equations(io_sys)) && error("Systems with algebraic equations are not supported") |
The updated example with your comments works well: using ModelPredictiveControl
using ModelingToolkit
using ModelingToolkit: D_nounits as D, t_nounits as t
@mtkmodel Pendulum begin
@parameters begin
g = 9.8
L = 0.4
K = 1.2
m = 0.3
end
@variables begin
θ(t) # state
ω(t) # state
τ(t) # input
y(t) # output
end
@equations begin
D(θ) ~ ω
D(ω) ~ -g/L*sin(θ) - K/m*ω + τ/m/L^2
y ~ θ * 180 / π
end
end
@named mtk_model = Pendulum()
mtk_model = complete(mtk_model)
function generate_f_h(model, par, inputs, outputs)
(_, f_ip), dvs, _, io_sys = ModelingToolkit.generate_control_function(model, inputs, split=false; outputs=outputs)
any(ModelingToolkit.is_alg_equation, equations(io_sys)) && error("Systems with algebraic equations are not supported")
h_ = ModelingToolkit.build_explicit_observed_function(io_sys, outputs; inputs = inputs)
nx = length(dvs)
vx = string.(dvs)
function f!(dx, x, u, _)
f_ip(dx, x, u, par, 1)
nothing
end
function h!(y, x, _)
y .= h_(x, 1, par, 1)
nothing
end
return f!, h!, nx, vx
end
inputs, outputs = [mtk_model.τ], [mtk_model.y]
const par = (9.8, 0.4, 1.2, 0.3)
f!, h!, nx, vx = generate_f_h(mtk_model, par, inputs, outputs)
nu, ny, Ts = 1, 1, 0.1
vu, vy = ["\$τ\$ (Nm)"], ["\$θ\$ (°)"]
model = setname!(NonLinModel(f!, h!, Ts, nu, nx, ny); u=vu, x=vx, y=vy)
using Plots
u = [0.5]
N = 35
res = sim!(model, N, u)
display(plot(res, plotu=false))
α=0.01; σQ=[0.1, 1.0]; σR=[5.0]; nint_u=[1]; σQint_u=[0.1]
estim = UnscentedKalmanFilter(model; α, σQ, σR, nint_u, σQint_u)
const par_plant = (par[1], par[2], 1.25*par[3], par[4])
f_plant, h_plant, _, _ = generate_f_h(mtk_model, par_plant, inputs, outputs)
plant = setname!(NonLinModel(f_plant, h_plant, Ts, nu, nx, ny); u=vu, x=vx, y=vy)
res = sim!(estim, N, [0.5], plant=plant, y_noise=[0.5])
display(plot(res, plotu=false, plotxwithx̂=true))
Hp, Hc, Mwt, Nwt = 20, 2, [0.5], [2.5]
nmpc = NonLinMPC(estim; Hp, Hc, Mwt, Nwt, Cwt=Inf)
umin, umax = [-1.5], [+1.5]
nmpc = setconstraint!(nmpc; umin, umax)
res_ry = sim!(nmpc, N, [180.0], plant=plant, x_0=[0, 0], x̂_0=[0, 0, 0])
display(plot(res_ry))
res_yd = sim!(nmpc, N, [180.0], plant=plant, x_0=[π, 0], x̂_0=[π, 0, 0], y_step=[10])
display(plot(res_yd))
|
Where does this particular order of parameters come from? const par = (9.8, 0.4, 1.2, 0.3) it is most likely incorrect, the generated dynamics have parameters in this order
This is a safer way to get the parameters in the correct order julia> getindex.(Ref(defaults(io_sys)), psym)
4-element Vector{Float64}:
0.4
9.8
1.2
0.3 It is generally unsafe to assume the order of elements of any variable in MTK. The one exception is when the order is explicitly indicated by the user, such as the order of The final system that has all the required information about unknowns and variable order is |
But in which order does f_ip want its parameters? In the |
This is exactly why it is tricky to get things like this right with MTK ;)
in the |
Should be correct now: using ModelPredictiveControl
using ModelingToolkit
using ModelingToolkit: D_nounits as D, t_nounits as t, varmap_to_vars
@mtkmodel Pendulum begin
@parameters begin
g = 9.8
L = 0.4
K = 1.2
m = 0.3
end
@variables begin
θ(t) = 0.0 # state
ω(t) # state
τ(t) # input
y(t) # output
end
@equations begin
D(θ) ~ ω
D(ω) ~ -g/L*sin(θ) - K/m*ω + τ/m/L^2
y ~ θ * 180 / π
end
end
@named mtk_model = Pendulum()
mtk_model = complete(mtk_model)
function generate_f_h(model, inputs, outputs)
(_, f_ip), dvs, psym, io_sys = ModelingToolkit.generate_control_function(model, inputs, split=false; outputs=outputs)
any(ModelingToolkit.is_alg_equation, equations(io_sys)) && error("Systems with algebraic equations are not supported")
h_ = ModelingToolkit.build_explicit_observed_function(io_sys, outputs; inputs = inputs)
nx = length(dvs)
vx = string.(dvs)
@show par = varmap_to_vars(defaults(io_sys), psym)
function f!(dx, x, u, _)
f_ip(dx, x, u, par, 1)
nothing
end
function h!(y, x, _)
y .= h_(x, 1, par, 1)
nothing
end
return f!, h!, nx, vx
end
inputs, outputs = [mtk_model.τ], [mtk_model.y]
f!, h!, nx, vx = generate_f_h(mtk_model, inputs, outputs)
nu, ny, Ts = 1, 1, 0.1
vu, vy = ["\$τ\$ (Nm)"], ["\$θ\$ (°)"]
model = setname!(NonLinModel(f!, h!, Ts, nu, nx, ny); u=vu, x=vx, y=vy)
using Plots
u = [0.5]
N = 35
res = sim!(model, N, u)
display(plot(res, plotu=false))
α=0.01; σQ=[0.1, 1.0]; σR=[5.0]; nint_u=[1]; σQint_u=[0.1]
estim = UnscentedKalmanFilter(model; α, σQ, σR, nint_u, σQint_u)
mtk_model.K = defaults(mtk_model)[mtk_model.K] * 1.25
f_plant, h_plant, _, _ = generate_f_h(mtk_model, inputs, outputs)
plant = setname!(NonLinModel(f_plant, h_plant, Ts, nu, nx, ny); u=vu, x=vx, y=vy)
res = sim!(estim, N, [0.5], plant=plant, y_noise=[0.5])
display(plot(res, plotu=false, plotxwithx̂=true))
Hp, Hc, Mwt, Nwt = 20, 2, [0.5], [2.5]
nmpc = NonLinMPC(estim; Hp, Hc, Mwt, Nwt, Cwt=Inf)
umin, umax = [-1.5], [+1.5]
nmpc = setconstraint!(nmpc; umin, umax)
res_ry = sim!(nmpc, N, [180.0], plant=plant, x_0=[0, 0], x̂_0=[0, 0, 0])
display(plot(res_ry))
res_yd = sim!(nmpc, N, [180.0], plant=plant, x_0=[π, 0], x̂_0=[π, 0, 0], y_step=[10])
display(plot(res_yd))
|
generate_control_function works fine on the small model in this example, but when trying it on a bigger model (1757 equations) it gets extremely slow. Any idea why and what can I do about it? |
Is it slower than |
The problem is that the call never completes. I will try to use @profview and let it run overnight if you want. When I interrupt the function with ctrl+c it is stuck in |
1757 equations isn't terribly big, but the MTK compiler is known to scale quite poorly. If you're an academic user, JuliaSimCompiler could potentially be an option to make it significantly faster. Unfortunately, |
The model is public, it is part of KiteModels.jl. It is not part of the main branch yet, but you can add the branch: julia> ] add https://github.com/ufechner7/KiteModels.jl/tree/feat/xfoil-polars And the code I am trying to run: But don't worry if it is too much work to run the model, I can also wait for the function to be merged. |
It was just merged, but we need a new release as well for it to become available. I tried loading your model but couldn't get past Once a new release of JSCompiler is made, you can use it like so using JuliaSimCompiler
(_, f_ip), dvs, psym, io_sys = ModelingToolkit.generate_control_function(IRSystem(model), inputs, split=false) |
@baggepinnen You need to call:
which is commented in the code. This command assumes you execute the script kite.jl with
Giving the absolute path will not work. |
I did try that, but I think these functions assume something about the local paths that does not hold the way I installed things. Anyway, you can already now try using JuliaSimCompiler
structural_simplify(IRSystem(model), (inputs, [])) this is the internal processing that takes place inside of |
The |
kite_model, inputs = KiteModels.model!(kite, pos, vel)
inputs, outputs = inputs, [kite_model.pos[2, kite.num_A]]
@time structural_simplify(kite_model, (inputs, [])) results:
Somehow, structural_simplify is way slower inside generate_control_function. @time (_, f_ip), dvs, psym, io_sys = ModelingToolkit.generate_control_function(model, inputs, split=false; outputs=outputs)
8306.809924 seconds (5.55 G allocations: 260.320 GiB, 80.75% gc time) How is this possible? Structural_simplify is being called with the exact same arguments and model inside generate_control_function. |
@baggepinnen I submitted an issue for this here: SciML/ModelingToolkit.jl#3077 |
My solution for now: splitting up into two functions and using Serialization function get_control_function(model, inputs; filename="control_function.bin")
if isfile(filename)
println("deserializing control function")
return deserialize(filename)
else
println("generating control function: sit back, relax and enjoy...")
@time (_, f_ip), dvs, psym, io_sys = ModelingToolkit.generate_control_function(model, inputs; split=false)
any(ModelingToolkit.is_alg_equation, equations(io_sys)) && error("Systems with algebraic equations are not supported")
println("serializing control function")
@time serialize(filename, (f_ip, dvs, psym, io_sys))
return (f_ip, dvs, psym, io_sys)
end
end
function generate_f_h(inputs, outputs, f_ip, dvs, psym, io_sys)
@time h_ = ModelingToolkit.build_explicit_observed_function(io_sys, outputs; inputs = inputs)
nx = length(dvs)
vx = string.(dvs)
@show par = varmap_to_vars(defaults(io_sys), psym)
function f!(dx, x, u, _)
f_ip(dx, x, u, par, 1)
nothing
end
function h!(y, x, _)
y .= h_(x, 1, par, 1)
nothing
end
return f!, h!, nx, vx
end |
@1-Bart-1 @baggepinnen so what would be the most reliable chunk of code to add in the manual as an exemple of MTK integration on a simple nonlinear model ? P.S. I almost finished the breaking changes for v1.0.0. The The breaking change will add a model parameter (any type) in the signature: f(x, u, d, p) -> xdot # or f!(xdot, x, u, d, p) -> nothing
h(x, d, p) -> y # or h!(y, x, d, p) -> nothing A similar breaking change is introduced in the custom economic function |
This one should be the most reliable / simple. Uses the same pendulum problem as the nonlinear example in the documentation.
|
Thanks a lot @1-Bart-1 ! |
No problem :) I updated the code above to reflect the breaking change (just added an extra underscore to the arguments in f and h). |
I found a mistake in the example code... function h!(y, x, _, _)
y .= h_(x, 1, par, 1)
nothing
end This should be changed to account for different input lengths: function h!(y, x, _, _)
y .= h_(x, ones(Int, length(inputs)), par, 1)
nothing
end And do I understand correctly that the output/observed variable cannot be directly dependent on the input? Because the input variables are not passed as an argument to h!. |
What are these ones doing there in the first place? The second argument to h from MTK is supposed to be u |
The ModelPredictiveControl.jl h! function does not have a u argument. As I understand it, MPC.jl assumes the output only depends on state variables, and not on input variables. This should hold true for most control functions. In the pendulum example, the output y only depends on the state x, and not on input u. It does not matter which numbers you pass to the second argument, because y does not depend on u. |
Ok, in that case it would probably be good to mention this in the example and also possibly error if the output does depend on inputs. Maybe passing |
How can you check if outputs depend on inputs? And @franckgaga are there any situations where this would happen? If so, shouldn't h! and h have an argument u? |
High-pass systems and systems with acceleration as output often have outputs that depend on inputs |
But does it help to make an error message if MPC.jl doesn't support outputs that depend on inputs anyways? On a different note: julia> par = ModelingToolkit.varmap_to_vars(defaults(io_sys), psym; check=false)
ERROR: KeyError: key g not found
julia> typeof(io_sys)
JuliaSimCompiler.ScheduledSystem{Nothing, JuliaSimCompiler.DiscreteInfo{Nothing, Nothing}}
julia> typeof(psym)
Vector{IRElement} (alias for Array{JuliaSimCompiler.ADT.IRElement, 1})
julia> defaults(io_sys)
Dict{Any, Any} with 8 entries:
m => 0.3
L => 0.4
θ(t) => 0.0
τ(t) => 0.0
g => 9.8
K => 1.2
ω(t) => 0.0
y(t) => 0.0
julia> psym
4-element Vector{JuliaSimCompiler.ADT.IRElement}:
g
L
m
K
julia> defaults(io_sys)[psym[1]]
ERROR: KeyError: key g not found |
Yes, because the MTK model has no issues with having outputs depending on inputs. If the user uses this script with such a model, they will now silently get the wrong answer where an arrays of ones is used as input. For the varmap issue, use
Instead |
I get the same error when using defaults(mtk_model) and defaults(ir_model). |
No, this package does not support model outputs
If we do not neglect the aspects above in the modelling, the discretized version of these systems will have zero direct transmission from the inputs |
From that perspective, sending |
You are of course correct in what you say, but I still wouldn't be so quick in dismissing the direct feedthrough use case. You are not considering the following Not all systems that are controlled by mpc are purely physical systems. A very common case that isn't is when an mpc controller is used in a slower outer loop, and a much faster inner controller is tracking references provided by mpc. In this case, you cannot form any reasonable model of the inner closed loop without direct feedthrough. The inner loop in this case will interpolate between the samples in the reference trajectory in a non causal way, and the inner loop is in this case best discretized using a Tustin or FOH discretization which introduces nonzero D matrices. You can of course add a one sample delay, but if you're using a cascade controller because it's too slow to run mpc at the faster rate, an additional sample delay at the slow rate is a very poor solution to an artificial problem introduced by the MPC tool. |
Any suggestions for how to solve this @baggepinnen ? |
I can check carefully tomorrow, but for now, check if |
They are not the same. julia> parameters(mtk_model)
4-element Vector{SymbolicUtils.BasicSymbolic{Real}}:
L
g
K
m
julia> psym
4-element Vector{JuliaSimCompiler.ADT.IRElement}:
g
L
m
K
julia> parameters(ir_model)
4-element Vector{JuliaSimCompiler.ADT.IRElement}:
L
g
K
m
julia> parameters(io_sys)
5-element Vector{JuliaSimCompiler.ADT.IRElement}:
g
L
m
K
τ |
In that case, you can maybe use |
Could you check as well how to run |
The same as for an |
Thanks, this is the updated code using IRSystem JSC: function get_control_function(model, inputs)
println("generating control function")
f_ip, dvs, psym, io_sys = ModelingToolkit.generate_control_function(IRSystem(model), inputs)
# any(ModelingToolkit.is_alg_equation, equations(io_sys)) && error("Systems with algebraic equations are not supported")
return (f_ip, dvs, psym, io_sys)
end
function generate_f_h(sys, inputs, outputs, f_ip, dvs, psym)
h_ = JuliaSimCompiler.build_explicit_observed_function(sys, outputs; inputs = inputs, target = JuliaSimCompiler.JuliaTarget())
nu = length(inputs)
ny = length(outputs)
nx = length(dvs)
vu = string.(inputs)
vy = string.(outputs)
vx = string.(dvs)
par = JuliaSimCompiler.initial_conditions(io_sys, defaults(io_sys), psym)[2]
function f!(dx, x, u, _, _)
f_ip(dx, x, u, par, 1)
nothing
end
function h!(y, x, _, _)
y .= h_(x, fill(nothing, length(inputs)), par, 1)
nothing
end
return f!, (h!, nu, ny, nx, vu, vy, vx)
end One last comment on the example code: the order of the states is determined by io_sys. So to set an initial state, it is better to use something like this: x_0 = zeros(nx)
x̂_0 = zeros(nx + ny)
x_0[ModelingToolkit.variable_index(io_sys, :θ)] = π
x̂_0[ModelingToolkit.variable_index(io_sys, :θ)] = π
res_yd = sim!(nmpc, N, [180.0], plant=plant, x_0=x_0, x̂_0=x̂_0, y_step=[10]) |
I'm a little bit lost here, since I don't know what is JuliaSimCompiler (is it a proprietary tool?). Should I need to update the example in the manual with this code ? |
Yeah JSC is a proprietary tool, no need to update the example with JSC, we discussed it here only since JSC is better able to handle very large systems that @1-Bart-1 tried to use this functionality on. |
baggepinnen wrote on Discourse what you need to do if you want to use an MTK model with this package:
As a first step it would be nice to have a simple, working example for this.
The text was updated successfully, but these errors were encountered: