The AbstractParameterizedFunction Interface
AbstractParameterizedFunctions are ways for functions to hold parameters in ways that the solvers can directly solve the function, yet parameter estimation routines can access and change these values as needed. The interface has the following functions:
param_values(pf::AbstractParameterizedFunction) = # Get the values of the parameters num_params(pf::AbstractParameterizedFunction) = # Get the number of the parameters set_param_values!(pf::AbstractParameterizedFunction,params) = # Set the parameter values using an AbstractArray
AbstractParameterizedFunctions can be constructed in the two ways below.
The easiest way to make a
ParameterizedFunction is to use the constructor:
pf = ParameterizedFunction(f,params)
The form for
params is any type which defines the parameters (it does not have to be an array, and it can be any user-defined type as well). The resulting
ParameterizedFunction has the function call
pf(t,u,params,du) which matches the original function, and a call
pf(t,u,du) which uses internal parameters which can be used with a differential equation solver. Note that the internal parameters can be modified at any time via the field:
pf.p = ....
An additional version exists for
f(t,u,params) which will then act as the not in-place version
f(t,u) in the differential equation solvers.
Note that versions exist for the other types of differential equations as well. There are
pf = DAEParameterizedFunction(f,params) pf = DDEParameterizedFunction(f,params)
for DAEs and DDEs respectively. For DAEs, the in-place syntax is
f(t,u,params,du,out) and the not in-place syntax is
f(t,u,params,du). For DDEs, the in-place syntax is
f(t,u,h,params,du) and the not in-place syntax is
Examples using the Constructor
function pf_func(t,u,p,du) du = p * u - p * u*u du = -3 * u + u*u end pf = ParameterizedFunction(pf_func,[1.5,1.0])
pf can be used in the differential equation solvers and the ecosystem functionality which requires explicit parameters (parameter estimation, etc.).
Note that the not in-place version works the same:
function pf_func2(t,u,p) [p * u - p * u*u;-3 * u + u*u] end pf2 = ParameterizedFunction(pf_func2,[1.5,1.0])
Function Definition Macros
DifferentialEquations.jl provides a set of macros for more easily and legibly defining your differential equations. It exploits the standard notation for mathematically writing differential equations and the notation for "punching differential equations into the computer"; effectively doing the translation step for you. This is best shown by an example. Say we want to solve the ROBER model. Using the
@ode_def macro from ParameterizedFunctions.jl, we can do this by writing:
using ParameterizedFunctions f = @ode_def ROBERExample begin dy₁ = -k₁*y₁+k₃*y₂*y₃ dy₂ = k₁*y₁-k₂*y₂^2-k₃*y₂*y₃ dy₃ = k₂*y₂^2 end k₁=>0.04 k₂=>3e7 k₃=>1e4
This looks just like pseudocode! The macro will expand this to the "standard form", i.e. the ugly computer form:
f = (t,u,du) -> begin du = -0.04*u + 1e4*u*u du = 0.04*u - 3e7*u^2 - 1e4*u*u du = 3e7*u^2 end
Note that one doesn't need to use numbered variables: DifferentialEquations.jl will number the variables for you. For example, the following defines the function for the Lotka-Volterra model:
f = @ode_def LotkaVolterraExample begin dx = a*x - b*x*y dy = -c*y + d*x*y end a=>1.5 b=>1.0 c=>3.0 d=1.0
The macro is a Domain-Specific Language (DSL) and thus has different internal semantics than standard Julia functions. In particular:
Control sequences and conditionals (while, for, if) will not work in the macro.
Intermediate calculations (likes that don't start with
d_) are incompatible with the Jacobian etc. calculations.
The macro has to use
tfor the independent variable.
Functions defined using the
@ode_def macro come with many other features. For example, since we used
c, these parameters are explicitly saved. That is, one can do:
f.a = 0.2
to change the parameter
0.2. We can create a new function with new parameters using the name we gave the macro:
g = LotkaVolterraExample(a=0.3,b=20.3)
In this case,
c will default to the value we gave it in the macro.
Since the parameters are explicit, these functions can be used to analyze how the parameters affect the model. Thus ParameterizedFunctions, when coupled with the solvers, forms the backbone of functionality such as parameter estimation, parameter sensitivity analysis, and bifurcation analysis.
Extra Little Tricks
There are some extra little tricks you can do. Since
@ode_def is a macro, you cannot directly make the parameters something that requires a runtime value. Thus the following will error:
vec = rand(1,4) f = @ode_def LotkaVolterraExample begin dx = ax - bxy dy = -cy + dxy end a=>vec b=>vec c=>vec d=vec
To do the same thing, instead initialize it with values of the same type, and simply replace them:
vec = rand(1,4) f = @ode_def LotkaVolterraExample begin dx = ax - bxy dy = -cy + dxy end a=>1.0 b=>1.0 c=>1.0 d=vec f.a,f.b,f.c = vec[1:3]
Notice that when using
=, it can inline expressions. It can even inline expressions of time, like
d=2π. However, do not use something like
d=3*x as that will fail to transform the
In addition, one can also use their own function inside of the macro. For example:
f(x,y,d) = erf(x*y/d) NJ = @ode_def FuncTest begin dx = a*x - b*x*y dy = -c*y + f(x,y,d) end a=>1.5 b=>1 c=3 d=4
will do fine. The symbolic derivatives will not work unless you define a derivative for
Because the ParameterizedFunction defined by the macro holds the definition at a symbolic level, optimizations are provided by SymEngine. Using the symbolic calculator, in-place functions for many things such as Jacobians, Hessians, etc. are symbolically pre-computed. In addition, functions for the inverse Jacobian, Hessian, etc. are also pre-computed. In addition, parameter gradients and Jacobians are also used.
Normally these will be computed fast enough that the user doesn't have to worry. However, in some cases you may want to restrict the number of functions (or get rid of a warning). Macros like
@ode_def_nohes turn off the Hessian calculations, and
@ode_def_noinvjac turns off the Jacobian inversion. For more information, please see the ParameterizedFunctions.jl documentation.
Finite Element Method Macros
The other macro which is currently provided is the
@fem_def macro. This macro is for parsing and writing FEM functions. For example, in the FEM methods you have to use
x[:,1] instead of
x[:,2] instead of
y. The macro will automatically do this replacement, along with adding in parameters. Since FEM functions are more general, we also have to give it the function signature. Using the macro looks like this:
f = @fem_def (x) DataFunction begin sin(α.*x).*cos(α.*y) end α=>π a = 2π b = 8π*π gD = @fem_def (x) DirichletBC begin sin(α.*x).*cos(α.*y)/β end α=>a β=>b
This is equivalent to the definition:
f(x) = sin(2π.*x[:,1]).*cos(2π.*x[:,2]) gD(x) = sin(2π.*x[:,1]).*cos(2π.*x[:,2])/(8π*π)
The true power comes in when dealing with nonlinear equations. The second argument, which we skipped over as
(), is for listing the variables you wish to define the equation by. Mathematically you may be using
w, etc., but for array-based algorithms you need to use
u[:,2],etc. To avoid obfuscated code, the
@fem_def macro does this conversion. For example:
l = @fem_def (t,x,u) begin du = ones(length(u))-α*u dv = ones(length(v))-v end α=>0.5
says there are two equations, one for
ones(length(u))-α*u) and one for
(ones(length(v))-v). This expands to the equation:
l = (t,x,u) -> [ones(size(x,1))-.5u[:,1] ones(size(x,1))-u[:,2]]
When you have 10+ variables, using
@fem_def leads to code which is much easier to read!