JuliaDynamics / DynamicalSystems.jl

Award winning software library for nonlinear dynamics and nonlinear timeseries analysis

Home Page:https://juliadynamics.github.io/DynamicalSystemsDocs.jl/dynamicalsystems/dev/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

basins_of_attraction fails at simple system (easy fix)

vitusbenson opened this issue · comments

Describe the bug
I have a very simple 3-dimensional dynamical systems for which I want to find the basins of attraction automatically.
It is a combination of double-well potentials in each dimension - leading to 8 stable fixed points (+-1, +-1, +-1), which should each have one of the octant of 3d-space as their basin of attraction. Oddly, basins_of_attraction just returns all basins correctly in the lower half of the space, it misses the upper half completely and returns -1 instead.

Minimal Working Example

using DynamicalSystems 
using DifferentialEquations
using IntervalArithmetic

@inline @inbounds function f(u, p, t)
    du1 = -u[1]^3 + u[1]
    du2 = -u[2]^3 + u[2]
    du3 = -u[3]^3 + u[3]
    return SVector{3}(du1, du2, du3)
end
# Jacobian=>
@inline @inbounds function f_jac(u, p, t)
    J = @SMatrix [(-3*u[1]^2+1) 0. 0.;
    0. (-3*u[2]^2+1) 0.;
    0. 0.  (-3*u[3]^2+1)]
    return J
end

ds = ContinuousDynamicalSystem(f, [-1., -1., -1.], [0.], f_jac)
xg = yg = zg = range(-2.0, 2.0; length = 101)
mapper = AttractorsViaRecurrences(ds, (xg, yg, zg))
basins, attractors = basins_of_attraction(mapper, (xg, yg, zg))

x = interval(-2,2)
y = interval(-2,2)
z = interval(-2,2)
box = x × y × z
fp, eigs, stable = fixedpoints(ds, box)

In the above example, for instance i get the following output:

julia> unique(basins[:,:,52])
1-element Vector{Int16}:
 -1

Thank you in advance :)

Package versions
Julia Version 1.7.2 (2022-02-06)

  [608a59af] ChaosTools v2.9.0
  [77a26b50] DiffEqNoiseProcess v5.12.0
  [0c46a032] DifferentialEquations v7.1.0
  [61744808] DynamicalSystems v2.3.0
  [587475ba] Flux v0.13.3
  [d1acc4aa] IntervalArithmetic v0.20.6

Thatnks for the good report. What does fp, eigs, stable = fixedpoints(ds, box) return?

Below comes the output.. I found a way to avoid the problem: If I set the length of the grid to an even number, e.g. xg = yg = zg = range(-2.0, 2.0; length = 100), then I get the correctly classified basins of the stable attractors. And no basins for the unstable fixpoints (as these lie exactly on the hyperplanes that are 0 in at least one dimension). But if there are gridcells centered on the unstable fixpoints as in the original post, then the basins algorithm fails for some basins of the stable fixpoints.

julia> fp
3-dimensional Dataset{Float64} with 27 points
  1.94063e-57   1.0           1.72462e-57
 -1.0          -1.0           1.0
  1.36518e-45  -1.0          -1.0
  1.94063e-57   1.0           1.0
  1.0           1.0          -1.0
  1.0          -1.0           1.75017e-41
  1.0          -1.0          -1.0
  1.36518e-45  -1.0           1.0
  1.0           1.36518e-45  -1.0
  1.94063e-57   1.72462e-57   1.0
  ⋮                          
 -1.0           1.36518e-45  -1.0
  1.0           1.0           1.0
  1.0          -1.0           1.0
 -1.0          -3.10652e-41  -3.10652e-41
 -1.0           1.0          -3.50035e-41
 -1.0           1.0          -1.0
  1.0           1.72462e-57   1.72462e-57
  1.36518e-45   1.49952e-46  -1.0
 -1.0          -1.0          -1.0
julia> stable
27-element Vector{Bool}:
 0
 1
 0
 0
 1
 0
 1
 0
 0
 0
 0
 ⋮
 0
 0
 0
 1
 1
 0
 0
 1
 0
 0
 1
julia> eigs
27-element Vector{Vector{ComplexF64}}:
 [-2.0 + 0.0im, 1.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, -2.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, -2.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, -2.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, 1.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, 1.0 + 0.0im]
 ⋮
 [-2.0 + 0.0im, -2.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, 1.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, -2.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, -2.0 + 0.0im]
 [-2.0 + 0.0im, 1.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, -2.0 + 0.0im]
 [-2.0 + 0.0im, 1.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, 1.0 + 0.0im, 1.0 + 0.0im]
 [-2.0 + 0.0im, -2.0 + 0.0im, -2.0 + 0.0im]

Oh that's the classical mistake of having the unstable fixed points directly on the set of initial conditions, which means that they are identified as attractors. (this makes perfect sense by the way, it's not a problem with the algorithm).

So, shall I close this?

Hi, regarding the -1 issue in the original post, it seemed related to a solver related issue. I used:

diffeq = (alg = Vern9(), reltol = 1e-9, abstol = 1e-9)
mapper = AttractorsViaRecurrences(ds, (xg, yg, zg); diffeq)

The algorithm found 27 fixed points (including those on the coinciding with the grid):


[0.0, 0.0, -1.0000000000209022]
[-0.9999999999990652, 0.9999999999790907, 0.0]
[0.0, -0.9999999999990652, 0.9999999999790907]
[0.9999999999790907, -0.9999999999990652, 0.0]
[0.9999999999851943, 0.0, 0.9999999999851943]
[0.0, 0.9999999999790907, -0.9999999999990652]
[0.0, 0.9999999999790583, 0.0]
[-1.0000000000120615, -1.0000000000120615, -1.0000000000120615]
[-0.9999999999977844, -0.9999999999977844, 0.9999999999792872]
[-0.9999999999990652, 0.0, 0.9999999999790907]
[0.0, 0.0, 0.9999999999790583]
[0.9999999999790907, 0.0, -0.9999999999990652]
[0.0, -1.0000000000209022, 0.0]
[0.9999999999852051, 0.9999999999852051, -1.0]
[0.0, 0.0, 0.0]
[0.9999999999792872, -0.9999999999977844, -0.9999999999977844]
[-0.9999999999977844, 0.9999999999792872, -0.9999999999977844]
[-1.0, 0.9999999999852051, 0.9999999999852051]
[-1.0000000000147977, 0.0, -1.0000000000147977]
[-1.0000000000209022, 0.0, 0.0]
[0.9999999999790583, 0.0, 0.0]
[0.0, -1.0000000000147977, -1.0000000000147977]
[-1.0000000000147977, -1.0000000000147977, 0.0]
[0.9999999999851943, 0.9999999999851943, 0.0]
[0.9999999999852051, -1.0, 0.9999999999852051]
[0.0, 0.9999999999851943, 0.9999999999851943]
[0.9999999999879023, 0.9999999999879023, 0.9999999999879023]

With the other grid I get 8 fixed points only as predicted:

[-0.9999999999985189, -0.9999999999985189, 0.9999999999791677]
[0.9999999999852051, 0.9999999999852051, -0.9999999999999356]
[0.9999999999852051, -0.9999999999999356, 0.9999999999852051]
[-0.9999999999999356, 0.9999999999852051, 0.9999999999852051]
[0.9999999999791677, -0.9999999999985189, -0.9999999999985189]
[0.9999999999879039, 0.9999999999879039, 0.9999999999879039]
[-0.9999999999985189, 0.9999999999791677, -0.9999999999985189]
[-1.000000000012085, -1.000000000012085, -1.000000000012085]

Well this makes it clear that this was not a problem with our algorithm, but rather finetuning the parameters used.

Actually, no, I want to think about this a bit when I have time. If the trajectory went to a fixed point anyways, why would we get -1? Is it really that the SimpleTsit5 solver would deviate the trajectory so strongly that we don't even end up at the correct fixed points? Sounds a bit unlikely... But could be true! Maybe @vitusbenson can analyze this a bit more, as it is their system we are using here, so they will have the most benefit by figuring this out!

Well you also changed the tolerances here. What about with the SimpleATsit5? (SimpleTsit5 is non-adaptive so it could definitely happen there if you choose a high enough dt)

The problem goes away with the change in tolerance:

diffeq = (reltol = 1e-9, abstol = 1e-9)

Still I don't know what happens when the default tolerances are set. I can dig that.

@ChrisRackauckas Sorry I misstyped, the default solver is SimpleATsit5 not SimpleTsit5. The adaptive version is used by default.

Thanks all, indeed it seems to be the solver SimpleATsit5 that introduces the issue. For instance

ds = ContinuousDynamicalSystem(f, [-1., -1., -1.], [0.], f_jac)
xg = yg = zg = range(-2.0, 2.0; length = 101)
mapper = AttractorsViaRecurrences(ds, (xg, yg, zg); diffeq = (alg = SimpleTsit5(), dt = 5.))
basins, attractors = basins_of_attraction(mapper, (xg, yg, zg))

gives me the expected output

julia> unique(basins[:,:,52])
9-element Vector{Int16}:
 19
 20
 21
 22
 23
 24
 25
 26
 27

Also working is diffeq = (alg = Vern9(), reltol = 1e-9, abstol = 1e-9). But diffeq = (reltol = 1e-9, abstol = 1e-9) is not working.

But @awage said that the problem goes away with high tolerances even in the default solver. In any case, let's please simplify and not use basins_of_attraction, but rather directly evolve an initial condition that you know should go to the missed fixed point, and see what this initial condition is doing? It is clear by now that the "problem" is not with basins_of_attraction but with the time evolution, so the more we use basins_of_attraction the more we complicate things but in an unecessary manner.

Problem in quotes, because this is not a real problem.

Actually I am not so sure about that. With my original code:

julia> attractors[19]
3-dimensional Dataset{Float64} with 1 points
 -1.0  -1.0  1.0
julia> basins[40,40,52]
-1
julia> tr = trajectory(ds, 10000, [xg[40], yg[40], zg[52]])
3-dimensional Dataset{Float64} with 1000001 points
 -0.44      -0.44      0.04
 -0.443556  -0.443556  0.0404014
 -0.447126  -0.447126  0.0408067
 -0.45071   -0.45071   0.0412161
 -0.454309  -0.454309  0.0416297
 -0.457921  -0.457921  0.0420473
 -0.461547  -0.461547  0.0424691
 -0.465185  -0.465185  0.0428952
 -0.468837  -0.468837  0.0433255
 -0.472501  -0.472501  0.0437601
  ⋮                    
 -1.0       -1.0       1.0
 -1.0       -1.0       1.0
 -1.0       -1.0       1.0
 -1.0       -1.0       1.0
 -1.0       -1.0       1.0
 -1.0       -1.0       1.0
 -1.0       -1.0       1.0
 -1.0       -1.0       1.0
 -1.0       -1.0       1.0

So the initial condition at indices 40,40,52 should go to attractor 19. And in the docs it is stated that trajectory also uses SimpleATsit5 as default.

With "problem" in quotes I agree, as for all practical manners it is solved for me now - so feel free to close! :)

Ah god damn it. So this is indeed related to the recurrences algorithm. So, how much did you play around with the parameters of the algorithm like basin hit thresholds, leaving off to infinity thresholds, etc? Can you repeat with increasing eg. mx_chk_lost ??

I did play around a little bit with the parameters, but it never changed. mx_chk_lost = 10000 gives the same results.

I have a theory. When the recurrence algorithm hits a unstable fixed point the SimpleATsit5 increases the step dt. I found a value dt=5.864383502047231e15 after some time. So for the next initial condition, the solver goes nuts and spits NaN. The step size is not updated after each initial condition.

I still have to test this theory!

Yes, theory confirmed:

integ = integrator(ds, [0. ,0. ,0.])
for k=1:10; step!(integ); end
integ.dt = 2.2876792454960986e12

I wonder if we should warn of this behavior somewhere.

It seems like a bug that reinit! doesn't reset dt. We should do a PR at SimpleDiffEq.jl that ensures that reinit! also resets dt.

There is a keyword argument for it. https://diffeq.sciml.ai/stable/basics/integrator/#SciMLBase.reinit!

reset_dt: Set whether to reset the current value of dt using the automatic dt determination algorithm. Default is (integrator.dtcache == zero(integrator.dt)) && integrator.opts.adaptive

And no, it should not be the default because the most common use cases (reinitialization of DAEs) don't necessarily want to do this.

Okay, then someone needs to do a PR in ChaosTools.jl so that our calls to reinit! use the keyword reset_dt = true. @vitusbenson this could be your first pr here!

code: https://github.com/JuliaDynamics/ChaosTools.jl/blob/master/src/basins/attractor_mapping_recurrences.jl

I have made the necessary PR.

I am assuming this to not be an issue in DS v3 and closing it. Re-open a new issue if problems remain!