I am running into a development issue when defining a new feature, dealing with how to "silently define types". Currently I have macros for things like:
f = @ode_define begin
dx = a*x - b*x*y
dy = -c*y + d*x*y
end a=>1.5 b=>1 c=3 d=1
which will expand to
f = (t,u,du) -> begin
du[1] = 1.5*u[1] - u[1]*u[2]
du[2] = -3*u[2] + u[1]*u[2]
end
These define functions for use in ODE solvers. Some features for the ODE solvers require knowing the parameters (i.e. sensitivity analysis requires taking parameter derivatives) so to make it more general I would like to inline the parameters defined with an =, and have a "named way of accessing" parameters defined with a =>. For example, I would like to instead expand the macro to:
f,p = (t,u,du,p) -> begin
du[1] = p.a*u[1] - p.b*u[1]*u[2]
du[2] = -3*u[2] + u[1]*u[2]
end
The question is, how do you define the parameters? I could have the macro also do:
type Parameters
a::Float64
b::Float64
end
p = Parameters(a,b)
then you could only use the macro once per session because each time it would need to define a parameter type. Is there a safe way for naming this type so that the macro could be repeatedly used? Essentially, the parameter type doesn't mean much other than the fact that it's a container for something that holds a and b, and it can be accessed by name (crucial because I plan on doing things which ask which parameters you'd like to do it to, like bifurcation plots). This is what I mean by "silently"- I'd like the macro to spit out a p where p.a and p.b works, without reference / without the user having to care about how this was defined. Also, this design path has one limitation in that types cannot be defined inside of functions, so this macro cannot be used to define an ODE function inside of a function.
Does Julia offer a good way around these issues that doesn't sacrifice performance? (I could use a dictionary, but that would cause a performance hit).
To summarize the design goal, I want something so that macros like the one I show in the beginning could write code that is like:
f,p = (t,u,du,p) -> begin
du[1] = p.a*u[1] - p.b*u[1]*u[2]
du[2] = -3*u[2] + u[1]*u[2]
end
type Parameters
a::Float64
b::Float64
end
p = Parameters(a,b)
to call
solve(f,p,y0)
to use in a solver that essentially does
for i = 1:numiterations
y = y + dt*f(t,u,du,p)
end
The problem is whether the proper form for p is a type, or whether Julia has something else to "call things with names". If it is a type, how do you get around the fact that types have to have unique names (so the macro can't just trivially expand it to type Parameters since each time it may have different fields) but what to auto-generate a type where you just know the field names?
gensym creates globally-unique names, which can be spliced in to a type-definition expression, for example:
Expr(:type, true, gensym(:parameter), Expr(:(::), :x, :Int))
I could use a dictionary, but that would cause a performance hit
Not entirely sure I understand the design goal here, but in your example you are wrapping the instance p in the closure. If you make the ODE name a macro parameter (@ode_def f = ...), then you could set up a mapping in some global dictionary from f => f_metadata with handles to p and whatever you might need to modify.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With