Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Fix different solutions #17

Merged
merged 6 commits into from Jul 31, 2017
Merged

WIP: Fix different solutions #17

merged 6 commits into from Jul 31, 2017

Conversation

devmotion
Copy link
Member

@devmotion devmotion commented Jul 13, 2017

I finally found the missing piece, and could fix #15 for BS3 algorithms. The fix can easily be extended to all other affected algorithms, however, it might be preferable to add it to OrdinaryDiffEq.jl.

Besides the already discussed separation of u and k in perform_steps! it is necessary to update the value of fsalfirst in k (if existent): for in-place functions this happens automatically via pointers, when fsalfirst is updated in https://github.com/JuliaDiffEq/OrdinaryDiffEq.jl/blob/master/src/integrators/integrator_utils.jl#L280; for not in-place functions fsalfirst is updated in https://github.com/JuliaDiffEq/OrdinaryDiffEq.jl/blob/master/src/integrators/integrator_utils.jl#L282, but this change is propagated to k only at the end of the next execution of perform_step! in https://github.com/devmotion/OrdinaryDiffEq.jl/blob/master/src/integrators/low_order_rk_integrators.jl#L43. Hence the interpolation, which depends on k, differs. This should apply to all not in-place algorithms which save fsalfirst in k and use its entry in k for interpolation; I could provoke the problem with Tsit5 and apply the same fix. Only the fix for BS3 is included here so far, since I was not sure whether a fix in OrdinaryDiffEq.jl would be more appropriate.

The tests pass locally but need SciML/OrdinaryDiffEq.jl#80.

# separate k to avoid influence of calculations on interpolation
# constant caches that contain fsalfirst must be updated for interpolation
# has to be changed all such caches - move to OrdinaryDiffEq?
if typeof(integrator.cache) <: Union{BS3ConstantCache}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just forgot to remove Union after experimenting with Tsit5 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this part for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To update k[1]. For some caches like BS3ConstantCache it contains fsalfirst - however, at this point of the algorithm its an old, outdated value of fsalfirst, and hence also any interpolation that uses k[1] is wrong. There are no problems with BS3Cache since it only points to fsalfirst and hence the value of k[1] always equals fsalfirst.

Of course, one has to add all affected caches (there are many more besides BS3ConstantCache - all not-inplace caches that contain fsalfirst and use it for interpolation) to this Union. I just wanted to wait til I know whether you would like to apply this update of k here or rather in OrdinaryDiffEq.jl when fsalfirst is updated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't this make k[1] == k[2] in the first round, making the interpolation/extrapolation invalid?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will make k[1] == k[2] in the first round. However, I'm still not completely sure about which values one would like to use for the interpolation - I mean, I know what we want to achieve but the problem is the correct assignment of u, uprev, t, and tprev for integrator.integrator. With the current (and the old) setup we have u == uprev (by the ODE algorithm) and t == tprev (by our assignments) in the first round. In the next rounds u is always updated to the newest estimate, uprev stays fixed, t is fixed to integrator.t + integrator.dt, i.e. the time point after this step, and tprev is fixed to integrator.t, i.e. the current time point.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should never have k[1] == k[2]. That would make it a constant extrapolation instead of the Hermite. In the first round it should stay in the previous setup, unchanged. After the first iteration, tprev = integrator.t, t = integrator.t + integrator.dt, uprev should be u(tprev), and u should be u(t)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's exactly one of the reasons why I was confused about this part. So the problem in our case is rather that for in-place algorithms with k[1] = integrator.fsalfirst and k[2] = integrator.fsallast (like BS3Cache) we always automatically have k[1] == k[2] at this point.... Maybe it's best to always separate k by integrator.integrator.k = recursivecopy(integrator.k) right after initialization and then use recursivecopy! in all other places? But I guess I have to try it to see whether it actually works...

# update ODE integrator
integrator.integrator.uprev = integrator.uprev
integrator.integrator.tprev = integrator.tprev
integrator.integrator.fsalfirst = integrator.fsalfirst
integrator.integrator.tprev = integrator.t # to extrapolate from current interval
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't update yet: it doesn't have valid values to extrapolate with.

Copy link
Member Author

@devmotion devmotion Jul 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah OK, then it is also incorrect on the master branch, since it just shortens lines 41 and 45 of the current implementation. The value at time integrator.t is integrator.uprev = integrator.u at this point - this is also not useful for the extrapolation... It should rather be integrator.uprev = u(integrator.tprev) (not updated tprev), shouldn't it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, was integrator.integrator.tprev never updated in the previous round? I think I'm going to have to run through it all again and print values out because it's definitely tricky.

# integrator.EEst = integrator.integrator.EEst
#end
# update ODE integrator
integrator.integrator.u = integrator.u
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To update the ODE integrator. Otherwise integrator.integrator.u still contains u(integrator.t), when peforming a step without iterations, or u(integrator.t+integrator.dt) after the second to last iteration, when performing fixed-point iterations. For any interpolations that occur after this step it should rather be the current, updated value u(integrator.t+integrator.dt).

@devmotion
Copy link
Member Author

Since both the current implementation and the first draft of this PR are not completely correct, I try to summarize both what we want to achieve and my suggestions/ideas, even though it is quite lengthy. I want to provide a rough overview of our desired algorithm using an example, hopefully as an assistance and a reminder to myself. Maybe it can also speed up the process of fixing the discovered problems. I surely made some mistakes, so please correct anything that is wrong.

Example

Assume we already calculated a solution u(t) up to time point t1, and the last step of our computation we moved from time point t0 to t1 such that [t0, t1] is the last calculated interval. Let dt be the proposed next time step, i.e. we would like to advance to time point t1 + dt in the next step. Moreover, let k[a, b](x, y) be the interpolation data of time interval [a, b] with function values x and y at a and b, respectively. Then our algorithm works in the following fashion:

  • DelayDiffEq.perform_steps!: When stepping into that method a subset of the variable assignments is given by

    Variable Value
    integrator.u usually u(t1) (see notes)
    integrator.uprev u(t1)
    integrator.t t1
    integrator.tprev t0
    integrator.k usually k[t0, t1](u(t0), u(t1)) (see notes)
    integrator.dt dt
    integrator.integrator.u u(t1)
    integrator.integrator.uprev u(t0)
    integrator.integrator.t t1
    integrator.integrator.tprev t0
    integrator.integrator.k k[t0, t1](u(t0), u(t1))

    Notes:

    • FSAL algorithms for in-place function already updated integrator.fsalfirst to integrator.fsallast in OrdinaryDiffEq.apply_step!, and hence if integrator.k[1] is set up to point to integrator.fsalfirst it holds integrator.k[1] == integrator.fsalfirst == integrator.fsallast at this point. Hence in that case integrator.k DOES NOT equal k[t0, t1](u(t0), u(t1)), and in particular integrator.integrator.k != integrator.k.
    • If we already calculated a value for u(t1+T) for some T > dt in the last iteration which was not accepted then integrator.u equals that value instead.

    We then calculate u(t1+dt):

    1. If dt is less or equal to the minimal lag of our DDE system we just apply the given ODE algorithm. Out of the variables above, only integrator.u and integrator.k are updated to u(t1+dt) and k[t1, t1 + dt](u(t1), u(t1 + dt)), respectively.

    2. In the other case we approximate u(t1 + dt) by a fixed-point iteration:

      • In the first iteration step we just apply the given ODE algorithm and obtain a first approximation u1(t1+dt).
      • For the next iteration steps we fix integrator.integrator.t, integrator.integrator.tprev, and integrator.integrator.uprev to t1 + dt, t1, and u(t1), respectively. Moreover, after the n-th iteration step we update integrator.integrator.u to un(t1 + dt) and integrator.integrator.k to k[t1, t1 + dt](u(t1), un(t1 + dt)). The fixed-point iteration is stopped if the maximal number of iterations is reached or the error estimate of the fixed point iteration, given by ||du / (abstol + reltol × U)|| where du = un(t1 + dt) - un-1(t1 + dt) and U = max(|un(t1 + dt)|, |un-1(t1 + dt)|), is below 1.

      When the fixed-point iteration is stopped after the m-th iteration the last approximation um(t1 + dt) and the corresponding interpolation data k[t1, t1 + dt](u(t1), um(t1 + dt)) are assigned to integrator.u and integrator.k. Moreover, we reset integrator.integrator.u, integrator.integrator.uprev, integrator.integrator.t, integrator.integrator.tprev, and integrator.integrator.k to the values before values before the fixed-point iteration, i.e. to u(t1), u(t0), t1, t0, and k[t0, t1](u(t0), u(t1)).

  • OrdinaryDiffEq.loopfooter!: Immediately after the calculation of u(t1 + dt) this method is executed, and hence the variable assignments look similar as above:

    Variable Value
    integrator.u u(t1 + dt)
    integrator.uprev u(t1)
    integrator.t t1
    integrator.tprev t0
    integrator.k k[t1, t1 + dt](u(t1), u(t1 + dt))
    integrator.dt dt
    integrator.integrator.u u(t1)
    integrator.integrator.uprev u(t0)
    integrator.integrator.t t1
    integrator.integrator.tprev t0
    integrator.integrator.k k[t0, t1](u(t0), u(t1))

    There are two cases:

    • If the step to time t1 + dt and value u(t1 + dt) is not accepted nothing changes.

    • If the step is accepted, integrator.t and integrator.tprev are updated to t1 + dt and t1. Moreover, the next step size dt1 is proposed and continuous and discrete callbacks are handled. Before, in between, or after callbacks are applied the method

      • DelayDiffEq.savevalues! is executed. It propagates changes to integrator introduced by the callbacks or just values of the before calculated time interval [t1, t1 + dt] to integrator.integrator, i.e. we update integrator.integrator.u to integrator.u, integrator.integrator.uprev to integrator.uprev, integrator.integrator.t to integrator.t, integrator.integrator.tprev to integrator.tprev, and integrator.integrator.k to integrator.k. Hence without callbacks integrator.integrator is just upated to the time interval [t1, t1 + dt]. The current values and the current interpolation of integrator.integrator are then used by
      • OrdinaryDiffEq.savevalues! to save values at time points before integrator.integrator.t, i.e. t1 + dt in the case without callbacks, to the solution integrator.integrator.sol (and implicitly also to integrator.sol since they both point to the same arrays). Moreover, at the end the interpolation data integrator.integrator.k is reduced to its minimal size integrator.integrator.kshortsize which depends on the chosen algorithm.

      If integrator.u, i.e. u(t1 + dt), is modified by a callback, DelayDiffEq.handle_callback_modifiers! marks integrator.fsalfirst for recalculation at the end of this step and updates the discontinuity tree of our system.

Now, if there are still stopping time points left but there is no stopping time point between t1 and integrator.t, i.e. t1 + dt without callbacks, we move on to

  • OrdinaryDiffEq.loopheader!:

    • If the step to t1 + dt was not accepted, the assignments noted in the table above are still in use. According to some formulas, depending on whether the calculated value u(t1 + dt) was out of the domain, and accounting for given maximal or minimal step sizes and stopping time points, integrator.dt is updated with a reduced time step ds < dt. We then continue at the top in DelayDiffEq.perform_step! to find a better calculation u(t1 + ds) using the variable assignment
    Variable Value
    integrator.u u(t1 + dt)
    integrator.uprev u(t1)
    integrator.t t1
    integrator.tprev t0
    integrator.k k[t1, t1 + dt](u(t1), u(t1 + dt))
    integrator.dt ds
    integrator.integrator.u u(t1)
    integrator.integrator.uprev u(t0)
    integrator.integrator.t t1
    integrator.integrator.tprev t0
    integrator.integrator.k k[t0, t1](u(t0), u(t1))
    • If the step to t1 + dt was accepted we go to

      • OrdinaryDiffEq.apply_step!: There integrator.uprev, which currently holds the value u(t1), is updated to the value of integrator.u, i.e. u(t1 + dt) without callbacks. Moreover, integrator.dt is changed from dt to dt1, i.e. we try to move to time point t1 + dt + dt1 in the next step. For FSAL algorithms the value of integrator.fsallast is copied to integrator.fsalfirst (so we can move on from u(t1 + dt)) or, if we are at a discontinuity or a callback changed the value of u(t1 + dt), integrator.fsalfirst is recalculated.

      • Similar to the case of not accepted steps, the given maximal or minimal step sizes and stopping time points are also taken into account, and integrator.dt might be updated with a reduced time step dt2 < dt1.

    Then, assuming no callbacks, the considered subset of variables is given by

    Variable Value
    integrator.u u(t1 + dt)
    integrator.uprev u(t1 + dt)
    integrator.t t1 + dt
    integrator.tprev t1
    integrator.k usually k[t1, t1 + dt](u(t1), u(t1 + dt)) (see notes above)
    integrator.dt dt2
    integrator.integrator.u u(t1 + dt)
    integrator.integrator.uprev u(t1)
    integrator.integrator.t t1 + dt
    integrator.integrator.tprev t1
    integrator.integrator.k k[t1, t1 + dt](u(t1), u(t1 + dt))

    We then continue at the top in DelayDiffEq.perform_step! to calculate u(t1 + dt2).

When we reached the end point, the solution is cleaned by

  • DelayDiffEq.postamble!: We update time point integrator.integrator.t, value integrator.integrator.u, and interpolation data integrator.integrator.k with their, maybe more up-to-date, corresponding fields of integrator, i.e. with integrator.t, integrator.u, and integrator.k. This method is executed after OrdinaryDiffEq.loopfooter!, and hence in the example above we would update integrator.integrator with the values t1 + dt, u(t1 + dt), and k[t1, t1 + dt](u(t1), u(t1 + dt)). If the values at time point integrator.integrator.t are not contained in the solution of integrator.integrator yet, they are added to the solution by OrdinaryDiffEq.postamble!.

Considerations

  • Time points integrator.t, integrator.tprev, integrator.integrator.t, and integrator.integrator.tprev as well as time step integrator.dt do not use pointers, hence they can always be updated by a simple assignment without any special undesired effects.

  • FSAL algorithms for in-place problems sometimes use interpolation data integrator.k whose first element integrator.k[1] points to integrator.fsalfirst, which in turn points to a field of integrator.cache (see e.g. BS3Cache). Since both integrator and integrator.integrator are initialized with the same cache integrator.cache then both integrator.fsalfirst and integrator.integrator.fsalfirst point to integrator.cache.fsalfirst, and both integrator.k[1] and integrator.integrator.k[1] point to integrator.cache.fsalfirst. Hence all these variables act on the same array, as long as they are not forcefully separated. This is a problem since most of the time we would like to have integrator.k != integrator.integrator.k, as shown above. Moreover, we want to update the interpolation data integrator.integrator.k only after a new estimate un(t1 + dt) whereas all in-place updates to integrator.k immediately influence integrator.integrator.k. Another issue is that FSAL algorithms for in-place function already update integrator.fsalfirst to integrator.fsallast in OrdinaryDiffEq.apply_step!, and hence if integrator.k[1] points to integrator.fsalfirst it holds integrator.k[1] == integrator.fsalfirst == integrator.fsallast at this point, and even worse, by the above argumentation also integrator.integrator.k[1] == integrator.fsalfirst == integrator.fsallast which corrupts the interpolation of integrator.integrator.

  • As seen, most of the time we should have integrator.u != integrator.integrator.u and integrator.uprev != integrator.integrator.uprev. If these variable are scalars we could still work with simple assignments, however, we quickly run into trouble if they point to the same arrays. So for arrays the best/easiest/most convenient option seems to be to separate those variables all the time, i.e. to let them point to different arrays all the time, and only update these arrays with recursivecopy! when needed.

  • At the end of the fixed point iteration we have to reset fields of integrator.integrator to the appropriate values in the time interval [t0, t1]. We can easily update the time points by integrator.integrator.t = integrator.t and integrator.integrator.tprev = integrator.tprev, and reset the value u(t1) with integrator.integrator.u = integrator.uprev. However, u(t0) and k[t0, t1](u(t0), u(t1)) can not be recovered from any variable, hence we have to cache them. To save allocations it might be desirable to add two additional fields, e.g. integrator.uprev_cache and integrator.k_cache, to the DDEIntegrator such that one can save and reset those values with recursivecopy! (if they are arrays, otherwise simple assignments work).

  • There is no need in calculating an error estimate of the fixed point iteration during its first iteration. The calculated value just classifies how far u(t1 + dt) differs from u(t1) or from a value u(t1 + dT) with dT > dt that was not accepted in last iteration. Because of this it might actually falsely accept values u(t1 + dt) close to those old values.

  • Does the error estimate integrator.EEst of the ODE solvers alone prevent that steps with poor error estimate in the fixed point iteration are accepted? The fixed point iteration might just return a value u(t1 + dt) with poor error estimate since it reached the maximum number of iterations. If this is a problem: since both the error estimate of the fixed point iteration and the error estimate integrator.EEst of the last iteration of the ODE solver are acceptable if they are not greater than 1, an easy solution would be to set integrator.EEst to max(integrator.EEst, fixedpointEEst).

@devmotion
Copy link
Member Author

I tried to implement exactly what I wrote down above. It seems to work quite well, I could improve some of the error bounds in the tests significantly. Partly this might be due to the improved extrapolation and due to the fact that the error estimate now combines both the error estimate of the integrator and the error estimate of the fixed-point iteration, which might lead to a higher rate of unaccepted time steps and thus sometimes smaller time steps.

It is even possible to use methods with lazy interpolants (#16) since additional steps are added to the interpolation before values are saved or a new step is calculated. I tested the problem prob_dde_1delay with Vern9, and the calculated steps are reasonable, however, the interpolation seems to be a bit odd - so there might still be a problem with those methods.

Two other problems I faced:

  • It can happen (e.g. in https://github.com/JuliaDiffEq/DelayDiffEq.jl/blob/master/test/unconstrained.jl#L67) that the fixed-point iteration blows up and finally ends up with u and EEst being NaN. OrdinaryDiffEq.jl then will try a new step size of value NaN in the next execution of perform_step!, which will just result in another u and EEst that are not numbers and hence in an infinite loop of iterations. Thus this has to be fixed and stopped immediately, and therefore the error estimate of the last iteration (which combines both the error estimate of the fixed-point iteration and of the integrator) and the initial interpolation data are cached, the fixed-point iteration is stopped if any NaN occur, and both the error estimate (which is responsible for the calculation of the next time step) and the interpolation data (which is used to calculate the next step) are reset to the cached values. With this change all tests can be run successfully.
  • I also tested for other methods like Tsit5, DP8, and among others the Verner methods whether they produce the same results for in-place and not in-place functions. Out of the methods I tested only Vern7 leads to different time steps and slightly different error estimates. Thus I tested whether this is reproducible with an ODE system, and indeed Vern7 produces different results for in-place and not in-place functions also with ODEs. So probably there is an error in one of the implementations of Vern7.

Moreover, with this changed implementation I get the best error estimates for my toy example so far; before (#15 (comment)) the error estimates were

julia> sol.errors
Dict{Symbol,Float64} with 3 entries:
  :l∞    => 3.33289e-5
  :final => 1.86351e-5
  :l2    => 1.40652e-5

julia> sol2.errors
Dict{Symbol,Float64} with 3 entries:
  :l∞    => 3.31446e-5
  :final => 1.86005e-5
  :l2    => 1.401e-5

now they are

julia> sol.errors
Dict{Symbol,Float64} with 3 entries:
  :l∞    => 2.98268e-5
  :final => 1.7883e-5
  :l2    => 1.30246e-5

julia> sol2.errors
Dict{Symbol,Float64} with 3 entries:
  :l∞    => 2.98268e-5
  :final => 1.7883e-5
  :l2    => 1.30246e-5

The tests pass locally but need SciML/OrdinaryDiffEq.jl#80 which is merged but not released yet.

@coveralls
Copy link

coveralls commented Jul 21, 2017

Coverage Status

Coverage increased (+4.3%) to 82.383% when pulling 152d906 on devmotion:different_solutions into 1ca6931 on JuliaDiffEq:master.

@coveralls
Copy link

coveralls commented Jul 21, 2017

Coverage Status

Coverage increased (+4.3%) to 82.383% when pulling dba09ae on devmotion:different_solutions into 1ca6931 on JuliaDiffEq:master.

@coveralls
Copy link

coveralls commented Jul 22, 2017

Coverage Status

Coverage increased (+4.4%) to 82.474% when pulling 73ee9f5 on devmotion:different_solutions into 1ca6931 on JuliaDiffEq:master.

@devmotion
Copy link
Member Author

Tests pass on Windows 64bit, Linux, and Mac - but the stricter error bounds lead to failures on Windows 32bit. Is there a workaround without sacrificing the better bounds completely?

@devmotion
Copy link
Member Author

I still think that the outline above summarises what we want to do - but does the extrapolation really work correctly?

I found some papers regarding our approach, e.g. https://link.springer.com/article/10.1007/BF02072017 and http://www.sciencedirect.com/science/article/pii/S0168927497000226, and because of the obvious formula for Θ on p. 220 of Bellen I'm a bit worried that we might extrapolate incorrectly. My main concern is the top part about perform_step!, and especially the values we use to calculate the extrapolation.

First of all, every extra- and intrapolation depends on both the last time point of the integrator.integrator.sol and on the current and previous values of integrator.integrator (since current_interpolant calculates Θ as (t-integrator.integrator.tprev)/integrator.dt. Everything should be fine for any t before the current end point of the solution that just has to be interpolated, i.e. for all t in [t0, t1] in the example above. For these time points the history function will just use the interpolation of the solution integrator.integrator.sol, so we should not have to worry about it. But for any t after the current end point of the solution, i.e. any t that is greater than t1 in the example above, the history function will just call current_interpolant, which in turn defines Θ as (t-integrator.integrator.tprev)/integrator.integrator.dt.

So my question is: don't we have to update integrator.integrator.tprev and integrator.integrator.dt already to t1 (the last time point of the solution) and dt (the next time step) to calculate a meaningful Θ? As far as I see, this also would not affect any interpolations since they only depend on the solution integrator.integrator.sol and it would correspond to the formula given by Bellen.

But maybe I'm also just confused right now... It's really tricky to get it right it seems.

@ChrisRackauckas
Copy link
Member

ChrisRackauckas commented Jul 29, 2017

First of all, every extra- and intrapolation depends on both the last time point of the integrator.integrator.sol and on the current and previous values of integrator.integrator (since current_interpolant calculates Θ as (t-integrator.integrator.tprev)/integrator.dt. Everything should be fine for any t before the current end point of the solution that just has to be interpolated, i.e. for all t in [t0, t1] in the example above. For these time points the history function will just use the interpolation of the solution integrator.integrator.sol, so we should not have to worry about it. But for any t after the current end point of the solution, i.e. any t that is greater than t1 in the example above, the history function will just call current_interpolant, which in turn defines Θ as (t-integrator.integrator.tprev)/integrator.integrator.dt.

We just get a Θ > 1 which is perfectly fine. That form of extrapolation is common for producing the starting values of an implicit RK method. Then the interval is shifted using these starting values to generate a continuous solution over the next interval, and interpolating in there then makes Θ <= 1 for every interpolation.

I'd point to Hairer II or the papers on LSODE/Sundials as arguments for this as being a good starting scheme for implicit methods.

@devmotion
Copy link
Member Author

Ah OK, good to know. I am just worried there might still be some failures in the algorithm - in particular since suddenly the iterations were exploding for some special cases. And I was thinking whether one can stop the fixed point iterations as soon as the error estimate increases between two subsequent iterations.

@ChrisRackauckas
Copy link
Member

And I was thinking whether one can stop the fixed point iterations as soon as the error estimate increases between two subsequent iterations.

We should switch over to Anderson first, and this could probably be something in NLsolve.jl and we then interpret a retcode.

@ChrisRackauckas
Copy link
Member

ChrisRackauckas commented Jul 29, 2017

Where exactly are we on this PR? Ready for review again? Has some merge conflicts after the other PR

@devmotion
Copy link
Member Author

Finally I fixed the conflicts, so it is ready for review again. Took me awhile since I ran into PainterQubits/Unitful.jl#95 and I figured out that in fact the interpolation was done incorrectly - integrator.integrator.dt was never updated. This probably also caused the exploding fixed-point iterations, since they are gone now; but I did not remove the corresponding code, so the implementation should still be able to deal with exploding fixed-point iterations (in case they might happen).

Moreover, due to this change lazy interpolants do not work anymore, so for now I completely removed the code which was supposed to add additional steps and handle these algorithms.

@devmotion
Copy link
Member Author

Again HDF5 build errors on Windows 32bit but it seems these issues are common and known: JuliaPackaging/WinRPM.jl#120, JuliaPackaging/WinRPM.jl#118, JuliaPackaging/WinRPM.jl#107, and JuliaPackaging/WinRPM.jl#117

@coveralls
Copy link

Coverage Status

Coverage increased (+0.2%) to 82.313% when pulling e226e9a on devmotion:different_solutions into 9223d96 on JuliaDiffEq:master.

@coveralls
Copy link

coveralls commented Jul 30, 2017

Coverage Status

Coverage increased (+0.2%) to 82.313% when pulling e226e9a on devmotion:different_solutions into 9223d96 on JuliaDiffEq:master.

# update time interval of ODE integrator
integrator.integrator.t = integrator.t
integrator.integrator.tprev = integrator.tprev
integrator.integrator.dt = integrator.integrator.t - integrator.integrator.tprev
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not integrator.dt? This could be open to floating point issues.

src/solve.jl Outdated
@@ -75,7 +75,7 @@ function init(prob::AbstractDDEProblem{uType,tType,lType,isinplace}, alg::algTyp
ode_prob,
OrdinaryDiffEq.alg_order(alg)))
end
integrator.dt = dt
integrator.dt = zero(dt)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't this override the init dt choice from the user?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this just assures that the ODE integrator always satisfies tprev + dt == t. The DDE integrator is still initialized with the init dt choice from the user (or the calculated init dt).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting dt = t - tprev does not assure that the ODE integrator satisfies tprev + dt == t

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand you. Of course, for all other steps (besides the initial step) lines https://github.com/devmotion/DelayDiffEq.jl/blob/8008e8f2d0837c944e7a7093a948a4fb094f0bb5/src/integrator_utils.jl#L239-L242 and https://github.com/devmotion/DelayDiffEq.jl/blob/8008e8f2d0837c944e7a7093a948a4fb094f0bb5/src/integrator_interface.jl#L70-L72 guarantee that t, tprev, and dtof the ODE integrator integrator.integrator belong to the same time interval which is important for correct interpolation. So setting dt to zero initially (only for integrator.integrator, the DDE integrator still uses the supplied or calculated dt!) since tprev = t initially is not needed for any calculations but ensures this consistent relationship of these three fields throughout the whole algorithm.

Of course, because of floating point issues the subtraction is a bad idea, and hence I corrected this in the last commit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this actually have a practical impact, or just makes things nicer because then tprev + dt == t?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, for all other steps it makes a huge difference

Not this line though, just the PR in general?

So this specific line could be removed but I thought it would be preferable to keep dt, t, and tprev synchronized all the time.

Nah, it makes sense. Just was curious about the practical effect. In theory it would do a constant extrapolation if dt=0, I just was wondering if that ever came into play.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not this line though, just the PR in general?

Yes just the PR in general.

Nah, it makes sense. Just was curious about the practical effect. In theory it would do a constant extrapolation if dt=0, I just was wondering if that ever came into play.

But it is not implemented yet, is it? As far as I see, all calls of current_interpolant and current_interpolant! (and also of current_extrapolant and current_extrapolant!) will calculate a Θ that is not a number for dt=0 - which will always lead to interpolations/extrapolations that are not a number, I guess.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it is not implemented yet, is it? As far as I see, all calls of current_interpolant and current_interpolant! (and also of current_extrapolant and current_extrapolant!) will calculate a Θ that is not a number for dt=0 - which will always lead to interpolations/extrapolations that are not a number, I guess.

But almost all of the interpolations have u0 + dt* ..., so it'll "accidentally" work out as a constant extrapolation. I think just the SSPRK methods don't. But I don't think we're actually hitting this case so it probably doesn't come up. Probably worthwhile to address in another PR if it does, with the SSPRK methods/interpolants as the test case which doesn't have the accidental workaround.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But almost all of the interpolations have u0 + dt* ..., so it'll "accidentally" work out as a constant extrapolation.

I guess it won't, and it didn't in my tests with BS3 and Tsit5. The problem is that the expression in brackets after u0 + dt* is not a number in these cases; an example is line https://github.com/JuliaDiffEq/OrdinaryDiffEq.jl/blob/master/src/dense/interpolants.jl#L123.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, 0*NaN = NaN? I never knew that. Okay, then I guess that doesn't happen.

@codecov-io
Copy link

Codecov Report

Merging #17 into master will increase coverage by 0.22%.
The diff coverage is 95.52%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #17      +/-   ##
==========================================
+ Coverage   82.08%   82.31%   +0.22%     
==========================================
  Files           9        9              
  Lines         268      294      +26     
==========================================
+ Hits          220      242      +22     
- Misses         48       52       +4
Impacted Files Coverage Δ
src/solve.jl 92.94% <100%> (+0.53%) ⬆️
src/integrator_utils.jl 88.54% <100%> (+1.18%) ⬆️
src/integrator_type.jl 66.66% <100%> (ø) ⬆️
src/integrator_interface.jl 62.5% <93.61%> (+0.16%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9223d96...e226e9a. Read the comment docs.

@coveralls
Copy link

coveralls commented Jul 30, 2017

Coverage Status

Coverage increased (+0.3%) to 82.373% when pulling 8008e8f on devmotion:different_solutions into 9223d96 on JuliaDiffEq:master.

@ChrisRackauckas ChrisRackauckas merged commit 1c0d652 into SciML:master Jul 31, 2017
@devmotion devmotion deleted the different_solutions branch July 31, 2017 10:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Different solutions for in-place and not in-place functions with unconstrained algorithm
4 participants