Closed astrojuanlu closed 3 years ago
Yes. This is not a bug but a design choice (which might be a bad one :-) but at least it was a conscious one). The API is more similar to a class based API than a function based API. The solver has a state defined by N (t
, y
). N is 2 by default, but it can be larger if the method requires it for calculation (i.e. multistep methods). These (t
, y
) are ALWAYS the ones PROVIDED by the algorithm (e.g. in fixed step methods tn = t0 + n dt
)
When the user requires a t
y
is interpolated. Is this choice not ergonomic in your application?
Thanks for the explanation @hgrecco - I see that this is actually mentioned in the docs:
A solver instance remember and therefore the following command will not recalculate the what has happened between 0 and 10. [...] You can do this as many times as you want, as long as you move forward in time.
Regarding your question:
Is this choice not ergonomic in your application?
It's not a big deal, I just had to RTFM :) I was thinking if I could propose an improvement to the API or the error message, but now that I understand what run
does, it's quite evident.
Closing!
I just started using nbkode and I encountered the same ValueError message which leads me to here :D
By reading the upper posts(and the documents), I understand that this is a design choice, not a bug. it is clear.
But my question is: Does that mean I have to compile the solver every-each time if I change the initial conditions y0 (or input params)?
Say, put a scenario like this: I have got Sun, Earth and some 100,000 massless particles spread in the middle of space. I want to test their motion especially if their positions are in the neighborhood of Lagrangian Points. In this case, if the solver is a "jitted-function", then I can redo it with different y0s, even in parallel mode I think. In nbkode, do I need to compile the solver for each particle? (coz the compile time is relatively long ... ) Thanks!
Since I have very limited knowledge of numba, forgive me if this question is a "naive" one :)
BTW, nbkode is really great! it is easy to use, flexible as scipy does, and got some 200x runtime speedup in my test, and that is literally freaking me out! It really did help a lot. Thanks to @hgrecco and @maurosilber and all the contributors.
If you need to change the RHS function each time, yes (at least, until some of the issues linked in #5 are addressed in numba).
But if it is the same function, and only the initial conditions y0
change, it shouldn't recompile.
Note that you need to @numba.jit
the function beforehand. While the solver jits the function for you inside __init__
, it results in a "different" function, and it needs to recompile part of the solver. This jitting inside __init__
happens in two cases:
rhs(t, y, p)
), which can be passed with the params
argument, as a closure (rhs(t, y)
) is generated inside __init__
.Here is a minimal example:
import contextlib
import time
import numba
import nbkode
@contextlib.contextmanager
def timeit(message):
start = time.perf_counter()
yield
end = time.perf_counter()
total = end - start
print(f"{total:.2e} seconds", message)
@numba.njit # this is important
def func(t, y):
return -y
for i in range(3):
print("Loop", i)
with timeit("Initialize"):
solver = nbkode.RungeKutta23(func, 0, 1)
with timeit("First run"):
solver.run(5)
with timeit("Second run"):
solver.run(10)
which prints:
Loop 0
7.81e+00 seconds Initialize
../numbakit-ode/nbkode/core.py:617: NumbaExperimentalFeatureWarning: First-class function type feature is experimental
1.37e+01 seconds First run
1.06e-03 seconds Second run
Loop 1
1.05e-03 seconds Initialize
1.03e-03 seconds First run
1.10e-03 seconds Second run
Loop 2
1.04e-03 seconds Initialize
1.08e-03 seconds First run
1.05e-03 seconds Second run
@maurosilber Thanks for the example code! But with RHS functions coming with params, even those params are fixed values, the solver will be recompiled:
import contextlib
import time
import numba
import nbkode
@contextlib.contextmanager
def timeit(message):
start = time.perf_counter()
yield
end = time.perf_counter()
total = end - start
print(f"{total:.2e} seconds", message)
@numba.njit # this is important
def func(t, y, p):
return -y * p
for i in range(3):
print("Loop", i)
with timeit("Initialize"):
solver = nbkode.DOP853(func, 0, 1, params=2)
with timeit("First run"):
solver.run(5)
with timeit("Second run"):
solver.run(10)
It prints:
Loop 0
2.74e+00 seconds Initialize
.../nbkode/core.py:617: NumbaExperimentalFeatureWarning: First-class function type feature is experimental
8.38e+00 seconds First run
3.74e-04 seconds Second run
Loop 1
2.25e+00 seconds Initialize
.../nbkode/core.py:617: NumbaExperimentalFeatureWarning: First-class function type feature is experimental
8.37e+00 seconds First run
4.33e-04 seconds Second run
Loop 2
2.21e+00 seconds Initialize
.../nbkode/core.py:617: NumbaExperimentalFeatureWarning: First-class function type feature is experimental
8.53e+00 seconds First run
3.49e-04 seconds Second run
Yes, that's what I mentioned in item 2. But, you can do that closure outside the __init__
.
For instance,
@numba.njit
def func_with_params(t, y, p):
return -p * y
@numba.njit
def func(t, y):
return func_with_params(t, y, 2)
Or, more generally:
def param_closure(func, params):
@numba.njit
def rhs(t, y):
return func(t, y, params)
return rhs
rhs = param_closure(func_with_params, 2)
Oh! Right! It works! Finally, I got what you mean! Great Thanks for the example, really counting on it! Thanks again, nbkode rocks!
This code:
Has this effect:
This is Python 3.8, nbkode 0.3.