Description
The underlying problem is that we need to represent the generated f(du,u,p,t)
in a way where p
can hold the necessary parameters while also being a valid object for the optimal control. Without that, #1059 will not be able to generate an ODEProblem. But what kind of ODEProblem should it be generating? This issue is to specify the interface for the forward simulation and describe why that needs to be the case.
Proposed code
In a modified version of https://diffeqflux.sciml.ai/dev/examples/optimal_control/ where dx[1]
also has a parameter, i.e.
function dxdt_(dx,x,p,t)
x1, x2 = x
dx[1] = p.p[1] * x[2]
dx[2] = p.ctrl[1](t)^3
end
i.e. the parameters are held in some kind of struct:
ParameterController <: AbstractParameterData
p::P
ctrl::C
end
In this form, we need that each ctrl
is a ctrl(t)
which internally holds its parameterization. Then we would just build it like:
ODEProblem(sys,[x1 => ...], [p1 => ...],ctrls = [ctrl1 => DataInterpolations.ConstantInterpolation(data,data_t)])
Sounds good? Okay now the hard part.
Parameter Representation for Optimal Control
Now we want to optimize p
. That can mean multiple things: optimize p.p
, optimize p.ctrl
, or optimize p
in full. How do we make this happen? Well we can have p
itself act as a vector. If trainctrl == false
, then p[i] = p.p[i]
. If trainp == false
then p[i] == ...?
. So let's talk about that part.
We need to make the parameter interface recursive. ctrl
should be something that follows the interface that it acts like a parameter vector, so that ctrl[i]
is something that acts like a parameter vector, so that ctrl[i][j]
is something that acts like a parameter vector. If that's the case, we just map i
to continue over each next parameter vector.
DataInterpolations.jl was setup in 2018 with this in mind (gotta appreciate that forward thinking 🤔 😉 ) so if you did ConstantInterpolation{true}(u,t)
, the u
and t
would be trainable, and ConstantInterpolation(u,t) = ConstantInterpolation{false}(u,t)
would just make the values u
trainable and the node points t
of the controller would be fixed. To make this work, we would just need to have length(ConstantInterpolation(u,t))
work, and then paramlength(p) = paramlength(p.p) + paramlength(p.ctrl)
. Now if you have multiple controls, p.ctrl
can be a tuple of controls (or vector, depending on the type stability), and then paramlength(x::Tuple) = sum(paramlength,x)
. Then length(p::ParameterController ) = paramlength(p)
.
Given this indexing, pvec = Vector(p)
would "just work" via Julia's generic dispatches. However, we would still need a way to change a vector back into p
, which we should implement by overloading ArrayInterface.restructure(p,pvec)
(though it might just work by default?). Once this is the case, then all of GalacticOptim, adjoints, DiffEqFlux, etc. should be able to train the values of the parameters and the controllers in this form.
Pieces that are made to be constant would just be things that are kept out of the vectorization of the parameter struct.
Where to put it
Since this is a generally nice interface, it might make sense to have this be a package that we point people towards which defines a nice structure to parameters. This is also the general solution to a long-standing issue SciML/DiffEqFlux.jl#178 and SciML/SciMLSensitivity.jl#80, it might be good to call it SciMLParameters.jl or something and make it a full interface so that it's documented and people know how to make their parameters "fit" with all of the packages in the ecosystem. Having this would work make p = TupleParams(p1,p2)
work in DiffEqFlux, if it's using this paramlength
where on tuple
it knows to just extend. ModelingToolkit.jl would then just use this new "SciML common parameter interface"