Based on your questions, I would guess that (i) you are very new to Julia, and (ii) you have some but not much background in programming. That is ok – I’m not not a super programmer myself, and am trying to understand some of the intricacies of Julia. So: my answers are not the final answers – I may have misunderstood things myself. But let me try to answer some of your questions… from an amateur programmer:
- If you define a function which describes an ordinary differential equation (example: the
parameterized_lorenz function above) and want to use the differential equation solvers in the
DifferentialEquations.jl package, you have to use the specified function arguments. You can not just skip arguments. As
MatFi suggest, the only reason why your code worked, is because of Julia’s “multiple dispatch” feature.
- A simple-minded understanding of “multiple dispatch” (i.e., my understanding) is that it allows you to have several mappings with the same name, but with different behavior depending on the argument types. Because you already had defined the correct version with correct arguments in
parameterized_lorenz(du,u,p,t), when you define a second mapping with the same name but zero arguments (
parameterized_lorenz()) this does not replace the first mapping. Note that in, say, MATLAB or Python, this would replace the first function with the second one, but in Julia, instead, both mappings co-exist.
- Next, when you apply the function, Julie chooses the one which has the same arguments as needed.
Just as a stupid illustration, suppose I make three versions of a mapping
my_sin(x) = sin(x)
my_sin() = "Good evening"
my_sin(x::Vector) = sum(x);
The first and third versions have a single argument; the first one is chosen if the single argument is a scalar, while the third is chosen if the single argument is a vector. The second version is chosen if there is zero arguments. They all co-exist:
julia> my_sin(pi/2), my_sin(), my_sin([1,2,3])
(1.0, "Good evening", 6)
Similarly in your case: you first define function
parameterized_lorenz with arguments that
DifferentialEquations.jl can understand, next you define the same mapping with zero arguments – which doesn’t make sense to
DifferentialEquations.jl. Thus, when you define the problem via function
ODEProblem chooses to use the mapping with correct arguments. In other words, you haven’t really tested it with zero arguments. To test it with zero arguments, you need to close Julia, start Julia again and only define
parameterized_lorenz with zero arguments – before you call
ODEProblem. And then you will get an error message, because the correct arguments
(du,u,p,t) are required, and you have not defined the function with those arguments.
- What about “function”
ODEProblem? OK – here, I’m on “thin ice”. What I guess is happening is that the guys who developed
DifferentialEquations.jl created a data structure named
ODEProblem, a so-called
struct in Julia speak. A
struct is a way to store information which is user defined.
Suppose I define a
MyProblem, with 4 “fields”:
In Julia, the
struct name automatically is the “constructor” for the
struct. So I create a “problem” as follows:
prob = MyProblem(sin,-2pi,2pi,20)
I can read the field values of the data structure
I can now design a plotting function which operates on my
x = range(problem.x1,problem.xN,length=problem.N)
…and I can apply my function
my_plot on the data structure
which leads to the result:
OK – isn’t this just a complicated way of doing:
Yes it is!!
So why would one do such a thing? Well, in my stupid case: suppose I wanted to make several versions of the
my_plot function, one where I draw straight lines between the data points, another where I use quadratic interpolation, one where I use step function, etc., etc? Then I could create the data structure
prob and just change an option in the
my_plot function, say add a second argument:
Back to your Lorenz problem. What is going on is thus (I think):
A. You define the ordinary differential equation in the
parameterized_lorenz. This function is different for every model, but must have the parameters
(du,u,p,t) in the given order.
ODEProblem is really a “constructor” which pushes the argument values into fields in a data structure. In your case, the arguments of the constructor are
(parameterized_lorenz,u0,tspan,p). The first of these arguments (
parameterized_lorenz) is pushed into the first field of the data structure (which you have named
prob), and by the definition of the
ODEProblem data structure, this should be a function. The second argument is pushed into the field of the initial value. The third argument is pushed into the field of time span for the simulation, etc. Thus, the argument order in the constructor must be correct, or else the information is pushed to the wrong field in the data structure. Again, observe that the order of arguments which are pushed to fields in the constructor has nothing to do with the order of arguments in the function which defines the model.
ODEProblem only stores the information in a suitable data structure, nothing more.
C. Finally, the
DifferentialEquations.jl package has defined a function
solve with one required positional argument; the required argument is a data structure defining the model. When this argument is a data structure of type
solve knows how to pull out information from the data structure and apply the ode solver.
So why on earth go through the complexity of storing the model in a data structure? One reason is that it structures the algorithms. Another is that the
solve function has a second, optional positional argument which specifies the solver. So if I write:
the default solver is used. If I instead write:
Tsit5() solver is used. And I don’t have to write a complex list of arguments – the “problem”
prob is the same as before; I only change the solver.
If I want to use a fourth order Runge Kutta solver, I simply write:
I can also use the explicit Euler solver. However, this solver only works with a fixed step length, so I need to add a keyword argument with the step length:
Anyway, this was an attempt to explain what is going on when you solve ODEs. Hope it clarifies things (and that I have a correct understanding…). Attempting to explain it gave me a chance to figure out
structs… (although I only scratched the surface…).