I have a light ray moving through space-time, i.e. a curve in $\mathbb{R}^4$, parametrized by some variable λ. The exact trajectory, i.e. the coordinate functions $x^μ(λ)$ of the curve are given by some ODE $\frac{dx^μ}{dλ} = f(λ)$.
Now, I've got two requirements for my code to satisfy:
The results $x^μ(λ)$ must be given on a regular grid, i.e. the step size dλ must be the same everywhere.
I don't know the interval λ runs over beforehand, i.e. integration must end dynamically as soon as a certain condition $g(λ, x^μ(λ))$ is met (basically I'm testing whether the coordinates $x^μ(λ)$ are outside of some coordinate interval where I don't know the space-time's properties anymore).
Requirement 2 obviously excludes scipy.odeint(). This leaves me with the scipy.ode class which allows me to pass the break condition to the integrator via a solout callback if I choose the dopri5 or the dop853 algorithm.
It's a bit more complicated, though: Since these (and all other) integrators dynamically adjust their internal step size (which is kind of the point of not doing the integration manually) I cannot simply store the values being passed to the callback if I want to fulfill requirement 1. (Update: In fact, it doesn't make sense to do so, anyway. See my remark (*) below.) Instead, I still need to loop over λ and call the scipy.ode.integrate() method for each step $λ + dλ$. This, however, means that I must somehow stop the loop:
from scipy.integrate import ode
def deriv(λ, x, params):
# …
return dx_dλ
end_reached = False
def solout(λ, x):
if g(λ, x):
# Stop integration
# Returning -1 will NOT make solver.successful() return False, so
# we need to inform the integration loop (see below) separately.
nonlocal end_reached
end_reached = True
return -1
# Everything's ok. Move on.
return 0
λ0 = 0
x0 = …
params = …
dλ = 0.1
solver = ode(deriv)
solver.set_integrator('dopri5', nsteps=500)
solver.set_solout(solout)
solver.set_initial_value(x0, λ0)
solver.set_f_params(params)
λ = [λ0]
x = [x0]
while True:
solver.integrate(solver.t + dλ)
if not solver.successful() or end_reached:
break
λ.append(solver.t)
x.append(solver.y)
All in all, this is rather ugly. Not only is the logic basically all over the place (for instance I cannot test the condition in the loop where it would belong). But due to the solout callback (I suppose) it's also at least one order of magnitude slower than without one. Update: In fact, even without the callback, the dopri5 integrator is orders of magnitude slower than vode (at least for another ODE where I wanted to use this approach).
Am I missing something or is this really the only way to meet the above requirements? I also tried raising an exception in deriv() (and subsequently catching it in the loop) in order to stop the integrator. While this works for all integrators, it will still lead to a bunch of errors being printed to stdout.
(*) As far as I understand, the values passed to deriv() are not meant to be a reliable approximation but only the final value returned by the solver is. For instance, the solver might do overshooting to choose a sensible step size in view of (rapidly) changing slopes and to thereby arrive at a best estimate.
g = trueresulting in invalid data and, even worse, in excess work being done by the integrator. In case $g$ is met in the middle of a time step, integration should simply stop without adding another data point to the results (which would obviously destroy the regularity of the grid). – balu Nov 29 '14 at 13:06