Closed ValentinKaisermayer closed 7 months ago
Did you try using the Difference operator? (Didn't read through all of this)
Did you try using the Difference operator? (Didn't read through all of this)
I did not
There's not much documented on it right now (anything?) but that is the approach that was developed for this.
I have to say the doc string:
Represents a difference operator.
at https://github.com/JuliaSymbolics/Symbolics.jl/blob/c2a4e89cc919cd3a6c9e0b19300b9b8d8d03770a/src/difference.jl#L4 Is not very helpful. ๐
What exactly does Difference(t;dt=0.01)(x) ~ y
mean?
Is it x[t+0.01] = y[t]
?
x[t+0.01]-x[t] = y[t]
So i would have to write Difference(t;dt=0.01)(x) ~ f(t) - x
if I want to set x
to the value of f(t)
at some discrete time steps?
In other words, what is the intended way of implementing a first-order hold (FOH)?
How should I implement the Step
block such that the discrete time event is handled correctly?
My approach would be to put the RHS (i.e. offset + IfElse.ifelse(t < start_time, 0, height)
) in a callback and set the value of y
in the integrator to it. However, this approach needs y
to be a state, which I can enforce via Differential(t)(y) ~ 0
.
In generate_difference_cb()
this is done very similar.:
https://github.com/SciML/ModelingToolkit.jl/blob/6d31f4bba00a350b0d1ae1d7fff9483ae04bde39/src/systems/diffeqs/abstractodesystem.jl#L149
using ModelingToolkit, IfElse, DifferentialEquations, Plots
@parameters t
function Plant(;name, x0=zeros(2))
D = Differential(t)
sts = @variables x1(t)=x0[1] x2(t)=x0[2] y(t) u(t)
eqs= [
D(x1) ~ x2
D(x2) ~ -x1 - 0.5 * x2 + u
y ~ 0.9 * x1 + x2
]
ODESystem(eqs, t, sts, []; name)
end
function Step(;name, height=1.0, offset=0.0, start_time=0.0)
sts = @variables y(t)=0.0
eqs = [
y ~ offset + IfElse.ifelse(t < start_time, 0, height) # needs event to work properly
]
ODESystem(eqs, t, sts, []; name)
end
@named plant = Plant()
@named step_block = Step(start_time=1)
connections = [
plant.u ~ step_block.y
]
@named sys = compose(ODESystem(connections, t), [plant, step_block])
sys = structural_simplify(model)
prob = ODAEProblem(sys, [], (0, 10.0))
sol = solve(prob, Tsit5())
plot(sol, vars=[step_block.y, plant.y])
I do not see how the Difference
operator can help me in this case. Not all discrete control schemes need a difference equation. For instance a time discrete P-controller.
Nevertheless, I tried but got an error:
function DiscreteTimeStep(;name, height=1.0, offset=0.0, start_time=0.0, dt=0.1)
sts = @variables y(t)=0.0
D = Difference(t; dt)
eqs = [
D(y) ~ -y + offset + IfElse.ifelse(t < start_time, 0, height)
]
ODESystem(eqs, t, sts, []; name)
end
@named plant = Plant()
@named step_block = DiscreteTimeStep(start_time=1, dt=0.1)
connections = [
plant.u ~ step_block.y
]
@named sys = compose(ODESystem(connections, t), [plant, step_block])
sys = initialize_system_structure(sys)
prob = ODAEProblem(sys, [], (0, 10.0))
ERROR: LoadError: KeyError: key 6 not found
Stacktrace:
[1] getindex
@ .\dict.jl:482 [inlined]
[2] torn_system_jacobian_sparsity(sys::ODESystem)
@ ModelingToolkit.StructuralTransformations .julia\packages\ModelingToolkit\45oYI\src\structural_transformation\codegen.jl:83
[3] build_torn_function(sys::ODESystem; expression::Bool, jacobian_sparsity::Bool, checkbounds::Bool, kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
@ ModelingToolkit.StructuralTransformations .julia\packages\ModelingToolkit\45oYI\src\structural_transformation\codegen.jl:232
[4] build_torn_function
@ .julia\packages\ModelingToolkit\45oYI\src\structural_transformation\codegen.jl:189 [inlined]
[5] ODAEProblem{true}(sys::ODESystem, u0map::Vector{Any}, tspan::Tuple{Int64, Float64}, parammap::SciMLBase.NullParameters; kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
@ ModelingToolkit.StructuralTransformations .julia\packages\ModelingToolkit\45oYI\src\structural_transformation\codegen.jl:350
[6] ODAEProblem (repeats 2 times)
@ .julia\packages\ModelingToolkit\45oYI\src\structural_transformation\codegen.jl:341 [inlined]
[7] ODAEProblem(::ODESystem, ::Vararg{Any, N} where N; kw::Base.Iterators.Pairs{Union{}, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
@ ModelingToolkit.StructuralTransformations .julia\packages\ModelingToolkit\45oYI\src\structural_transformation\codegen.jl:333
[8] ODAEProblem(::ODESystem, ::Vararg{Any, N} where N)
@ ModelingToolkit.StructuralTransformations .julia\packages\ModelingToolkit\45oYI\src\structural_transformation\codegen.jl:333
I think the actual solution could be that IfElse.ifelse
generates the callback, as you hinted in #38 and #893.
So i would have to write Difference(t;dt=0.01)(x) ~ f(t) - x if I want to set x to the value of f(t) at some discrete time steps? In other words, what is the intended way of implementing a first-order hold (FOH)?
Yes, it's a little odd but it also a bit more naturally matches the differential forms, and it simplifies anyways.
That IfElse issue doesn't look related. It looks like a structural_simplify bug. It's all not documented yet for a reason ๐ , but if you're willing to keep building test cases that would be helpful.
but if you're willing to keep building test cases that would be helpful.
Sure! ๐
@variables t
function DiscreteSin(dt; name)
@variables y(t)=0.0
U = DiscreteUpdate(t; dt=dt)
eqs = [
U(y) ~ sin(t)
]
ODESystem(eqs, t, name=name)
end
function Plant(;name, x0=zeros(2))
D = Differential(t)
sts = @variables x1(t)=x0[1] x2(t)=x0[2] y(t) u(t)
eqs= [
D(x1) ~ x2
D(x2) ~ -x1 - 0.5 * x2 + u
y ~ 0.9 * x1 + x2
]
ODESystem(eqs, t, sts, []; name)
end
dt = 1
@named plant_block = Plant()
@named sin_block = DiscreteSin(dt)
@named model = ODESystem([plant_block.u ~ sin_block.y], systems=[sin_block, plant_block])
sys = structural_simplify(model)
prob = ODAEProblem(sys, [], (0.0, 10.0))
sol = solve(prob, Tsit5())
errors:
ERROR: LoadError: MethodError: no method matching extract_derivative(::Type{ForwardDiff.Tag{Base.Fix2{NonlinearFunction{false, RuntimeGeneratedFunctions.RuntimeGeneratedFunction{(Symbol("##arg#347"), Symbol("##arg#348")), ModelingToolkit.StructuralTransformations.var"#_RGF_ModTag", ModelingToolkit.StructuralTransformations.var"#_RGF_ModTag", (0x16a3eeb1, 0x218cb4b5, 0x9fd455e0, 0x71ae8127, 0x109ddc8c)}, LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED_NO_TIME), Nothing}, Tuple{Float64}}, Float64}}, ::SymbolicUtils.Mul{ForwardDiff.Dual{ForwardDiff.Tag{Base.Fix2{NonlinearFunction{false, RuntimeGeneratedFunctions.RuntimeGeneratedFunction{(Symbol("##arg#347"), Symbol("##arg#348")), ModelingToolkit.StructuralTransformations.var"#_RGF_ModTag", ModelingToolkit.StructuralTransformations.var"#_RGF_ModTag", (0x16a3eeb1, 0x218cb4b5, 0x9fd455e0, 0x71ae8127, 0x109ddc8c)}, LinearAlgebra.UniformScaling{Bool}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, typeof(SciMLBase.DEFAULT_OBSERVED_NO_TIME), Nothing}, Tuple{Float64}}, Float64}, Float64, 1}, Int64, Dict{Any, Number}, Nothing})
(test) pkg> st
Status `****\Project.toml`
[0c46a032] DifferentialEquations v6.20.0
[961ee093] ModelingToolkit v7.1.3
[91a5bcdd] Plots v1.23.6
I hacked this simple control loop together, maybe it is of help for someone.
using ModelingToolkit, DifferentialEquations, Plots
@variables t
D = Differential(t)
function Plant(;name)
sts = @variables x(t)=0 y(t)=0 u(t)=0
eqs= [
D(x) ~ u
y ~ x
]
ODESystem(eqs, t, sts, []; name)
end
function PController(;name, kp=1)
sts = @variables e(t)=0 u(t)=0
eqs= [
u ~ kp * e
]
ODESystem(eqs, t, sts, []; name)
end
function Feedback(;name)
sts = @variables y(t)=0 r(t)=0 e(t)=0
eqs= [
e ~ r - y
]
ODESystem(eqs, t, sts, []; name)
end
The time continuous model is easy:
@named plant = Plant()
@named controller = PController(kp=1)
@named feedback = Feedback()
@named model = ODESystem([plant.u ~ controller.u, feedback.y ~ plant.y, controller.e ~ feedback.e, feedback.r ~ 1], systems=[plant, controller, feedback])
sys = structural_simplify(model)
prob = ODEProblem(sys, [], (0.0, 10.0))
sol = solve(prob, Tsit5())
plot(sol, vars=[plant.u, plant.y])
For the time discrete part I had to do some tricks.
I tried the DiscreteUpdate
operator but it seems to be not fully fleshed out.
function DiscretePController(;name, kp=1, dt=1)
sts = @variables u(t)=0 e(t)=0
pars = @parameters kp=kp
U = DiscreteUpdate(t; dt)
eqs= [
U(u) ~ kp * e
]
ODESystem(eqs, t, sts, pars; name)
end
@named plant = Plant()
@named controller = DiscretePController(kp=1, dt=0.25)
@named feedback = Feedback()
@named model = ODESystem([plant.u ~ controller.u, feedback.y ~ plant.y, controller.e ~ feedback.e, feedback.r ~ 1], systems=[plant, controller, feedback])
sys = structural_simplify(model)
prob = ODEProblem(sys, [], (0.0, 10.0))
sol = solve(prob; callback=prob.kwargs[:callback])
ERROR: LoadError: type NamedTuple has no field callback
So I came up with this approach:
function DiscretePController(;name, kp=1, dt=1)
sts = @variables u(t)=0 e(t)=0
pars = @parameters kp=kp
eqs= [
D(u) ~ 0 # force u to be a state, apply control law in callback
]
ODESystem(eqs, t, sts, pars; name)
end
@named plant = Plant()
@named controller = DiscretePController(kp=1)
@named feedback = Feedback()
@named model = ODESystem([plant.u ~ controller.u, feedback.y ~ plant.y, controller.e ~ feedback.e, feedback.r ~ 1], systems=[plant, controller, feedback])
sys = structural_simplify(model)
prob = ODEProblem(sys, [], (0.0, 10.0))
indexof(sym,syms) = findfirst(isequal(sym),syms)
function cb!(int)
kp = 1
int.u[indexof(controller.u, states(sys))] = kp * prob.f.observed(feedback.e, int.u, int.p, int.t)
end
sol = solve(prob, Tsit5(); callback=PeriodicCallback(cb!, 0.25; initial_affect=true))
plot(sol, vars=[plant.u, plant.y])
The D(u) ~ 0
forces the controller output to be a state and be constant.
In the callback only states can be accessed. But via the observable function from the problem, I can access the control error e
using the values provided by the integrator.
I know this is a hack but maybe it is useful for someone.
See @baggepinnen 's many issues on this. The old version was deprecated
Let's say I want to simulate an MPC controller where the plant is an MTK system. I would need to access the current measurements and would have to modify control inputs via a callback in every time step according to some optimization problem.
For example a modified RC circuit:
Notice the
ControlledVoltageSource
implementation:where I had to make
V
astate
in order to be able to access its value in the solution. For this to work the equationD(V) ~ 0
has to be added. This is actually the solution from: https://discourse.julialang.org/t/update-parameters-in-modelingtoolkit-using-callbacks/63770 An unexperienced user might not be able to do this.The control input can not be a
parameter
or else I can not access it correctly in the solution but at the same time thecontrols
kwarg requires it to be one. For a linear MPC I would need the system matrixA
, input matrixB
and output matrixC
(and very rarely feedthrough matrixD
), i.e. the jacobi matrices with respect to the states, inputs and outputs of the sate and output equations at some operating point. MTK should be able to provide those.If I where to implement a time discrete PID controller or any other time discrete control strategy it would be pretty much the same.
A more Modelicaesk solution would be to define a connector for the inputs and outputs like:
and use it in the component like:
This would at least solve the issue of the additional equation, that one has to add for this to work. However, maybe I am doing something wrong but I have to access the variable in a funny way in the callback: