I believe lens is the best API for this. As I noted before, lens can be used to specify arbitrary nested fields along which you are taking the derivative, for example. This can be done without defining any methods and works with vanilla Julia objects without any use of symbolic math framework (although they are very useful, of course). Because of this flexibility, I use it anywhere I need to tweak model parameters/inputs. See this example in Bifurcations.jl where I specify the parameter of a plain named tuple by a simple lens (param_axis = @lens _.i
). SteadyStateFit.jl uses lenses as “input setter” (like the argument u of f(x, u, t; p) in the OP) and the model parameter specifier. I also created a toolkit called Kaleido.jl that can be used to, e.g., add a constraint to a model and re-parametrize a model using TransformVariables.jl.
A simple implementation ControlProblem
would be such that it generates an ODE parameter ControlProblemWrapper
like below on the fly:
struct ControlProblemWrapper
f! # original dynamics
p # original parameter
input # time-to-input mapping
lens # input specifier
end
function f!(du, u, p::ControlProblemWrapper, t)
q = set(p.p, p.lens, p.input(t))
p.f!(du, q, t)
end
which can be used as
# Model with input function
function model!(dx,x,p,t)
dx[1] = -p[1]*x[1] + p[2]*p[3]
end
p1 = ControlProblemWrapper(
model!,
(1.0, 2.0, 3.0), # 3.0 as a place holder
u(t) = t < 5 ? 0.0 : 1.0,
(@lens _[3]),
)
Now it is super easy to make other part of the model time-dependent:
p2 = ControlProblemWrapper(
model!,
(1.0, 2.0, 3.0),
u(t) = t < 5 ? 0.0 : 1.0,
(@lens _[1]), # make the first parameter time-dependent
)
Note that above lenses are very simple ones. You can use it to specify (@lens _.arbitrary.nested[:complex].fields[1])
.