Animation illustrating AttractorsViaRecurrences

The following Julia script inputs a 2D continuous time dynamical system and animates its time evolution while illustrating how AttractorsViaRecurrences works.

using Attractors, CairoMakie
using PredefinedDynamicalSystems
using OrdinaryDiffEq

# Set up dynamical system: bi-stable predator pray
function predator_prey_rule(u, p, t)
    r, c, μ, ν, α, β, χ, δ = p
    N, P = u
    common = α*N*P/(β+N)
    dN = r*N*(1 - (c/r)*N)*((N-μ)/(N+ν)) - common
    dP = χ*common - δ*P
    return SVector(dN, dP)
end

u0 = SVector(8.0, 0.01)
r = 2.0
# r, c, μ, ν, α, β, χ, δ = p
p = [r, 0.19, 0.03, 0.003, 800, 1.5, 0.004, 2.2]

diffeq = (alg = Rodas5P(), abstol = 1e-9, rtol = 1e-9)
ds = CoupledODEs(predator_prey_rule, u0, p; diffeq)

u0s = [ # animation will start from these initial conditions
    [10, 0.012],
    [15, 0.02],
    [12, 0.01],
    [13, 0.015],
    [5, 0.02],
]

density = 31
xg = range(-0.1, 20; length = density)
yg = range(-0.001, 0.03; length = density)
Δt = 0.1
grid = (xg, yg)
mapper = AttractorsViaRecurrences(ds, grid;
    Δt, consecutive_attractor_steps = 10, consecutive_basin_steps = 10, sparse = false,
    consecutive_recurrences = 100, attractor_locate_steps = 100,
)

##########################################################################

function animate_attractors_via_recurrences(
        mapper::AttractorsViaRecurrences, u0s;
        colors = ["#FFFFFF", "#7143E0","#0A9A84","#AF9327","#791457", "#6C768C", "#4287f5",],
        filename = "recurrence_algorithm.mp4",
    )

    grid_nfo = mapper.bsn_nfo.grid_nfo

    fig = Figure()
    ax = Axis(fig[1,1])

    # Populate the grid with poly! rectangle plots. However! The rectangles
    # correspond to the same "cells" of the grid. Additionally, all
    # rectangles are colored with an _observable_, that can be accessed
    # later using the `basin_cell_index` function. The observable
    # holds the face color of the rectangle!

    # Only 6 colors; need 3 for base, and extra 2 for each attractor.
    # will choose initial conditions that are only in the first 2 attractors
    COLORS = map(c -> Makie.RGBA(Makie.RGB(to_color(c)), 0.9), colors)

    function initialize_cells2!(ax, grid; kwargs...)
        # These are all possible outputs of the `basin_cell_index` function
        idxs = all_cartesian_idxs(grid)
        color_obs = Matrix{Any}(undef, size(idxs)...)
        # We now need to reverse-engineer
        for i in idxs
            rect = cell_index_to_rect(i, grid)
            color = Observable(COLORS[1])
            color_obs[i] = color
            poly!(ax, rect; color = color, strokecolor = :black, strokewidth = 0.5)
        end
        # Set the axis limits better
        mini, maxi = Attractors.minmax_grid_extent(grid)
        xlims!(ax, mini[1], maxi[1])
        ylims!(ax, mini[2], maxi[2])
        return color_obs
    end

    all_cartesian_idxs(grid::Attractors.RegularGrid) = CartesianIndices(length.(grid.grid))

    # Given a cartesian index, the output of `basin_cell_index`, create
    # a `Rect` object that corresponds to that grid cell!
    function cell_index_to_rect(n::CartesianIndex, grid::Attractors.RegularGrid)
        x = grid.grid[1][n[1]]
        y = grid.grid[2][n[2]]
        dx = grid.grid_steps[1]
        dy = grid.grid_steps[2]
        rect = Rect(x - dx/2, y - dy/2, dx, dy)
        return rect
    end

    color_obs = initialize_cells2!(ax, grid_nfo)

    # plot the trajectory
    state2marker = Dict(
        :att_search => :circle,
        :att_found => :dtriangle,
        :att_hit => :rect,
        :lost => :star5,
        :bas_hit => :xcross,
    )

    # This function gives correct color to search, recurrence, and
    # the individual attractors. Ignores the lost state.
    function update_current_cell_color!(cellcolor, bsn_nfo)
        # We only alter the cell color at specific situations
        state = bsn_nfo.state
        if state == :att_search
            if cellcolor[] == COLORS[1] # empty
                cellcolor[] = COLORS[2] # visited
            elseif cellcolor[] == COLORS[2] # visited
                cellcolor[] = COLORS[3] # recurrence
            end
        elseif state == :att_found
            attidx = (bsn_nfo.current_att_label ÷ 2)
            attlabel = (attidx - 1)*2 + 1
            cellcolor[] = COLORS[3+attlabel]
        end
        return
    end

    # Iteration and labelling
    ds = mapper.ds
    bsn_nfo = mapper.bsn_nfo
    u0 = current_state(ds)

    traj = Observable(SVector{2, Float64}[u0])
    point = Observable([u0])

    marker = Observable(:circle)
    lines!(ax, traj; color = :black, linewidth = 1)
    scatter!(ax, point; color = (:black, 0.5), markersize = 20, marker, strokewidth = 1.0, strokecolor = :black)

    stateobs = Observable(:att_search)
    consecutiveobs = Observable(0)
    labeltext = @lift("state: $($(stateobs))\nconsecutive: $($(consecutiveobs))")

    Label(fig[0, 1][1,1], labeltext; justification = :left, halign = :left, tellwidth = false)
    # add text  with options
    kwargstext = prod("$(p[1])=$(p[2])\n" for p in mapper.kwargs)
    Label(fig[0, 1][1, 2], kwargstext; justification = :right, halign = :right, tellwidth = false)


    # make legend
    entries = [PolyElement(color = c) for c in COLORS[2:end]]
    labels = ["visited", "recurrence", "attr. 1", "basin 1", "attr. 2", "basin 2"]

    Legend(fig[:, 2][1, 1], entries, labels)


    # %% loop
    # The following code is similar to the source code of `recurrences_map_to_label!`

    cell_label = 0
    record(fig, filename) do io
        for u0 in u0s
            reinit!(ds, copy(u0))
            traj[] = [copy(u0)]

            while cell_label == 0
                step!(ds, bsn_nfo.Δt)
                u = current_state(ds)

                # update FSM
                n = Attractors.basin_cell_index(u, bsn_nfo.grid_nfo)
                cell_label = Attractors.finite_state_machine!(bsn_nfo, n, u; mapper.kwargs...)

                state = bsn_nfo.state

                if cell_label ≠ 0 # FSM terminated; we assume no lost/divergence in the system
                    stateobs[] = :terminated

                    # color-code initial condition if we converged to attractor
                    # or to basin (even or odd cell label)
                    u0n = Attractors.basin_cell_index(u0, bsn_nfo.grid_nfo)

                    basidx = (cell_label - 1)
                    color_obs[u0n][] = COLORS[3+basidx]

                    # Clean up: all "visited" cells become white again
                    visited_idxs = findall(v -> (v[] == COLORS[2] || v[] == COLORS[3]), color_obs)
                    for n in visited_idxs
                        color_obs[n][] = COLORS[1] # empty
                    end
                    # clean up trajectory line
                    traj[] = []

                    for i in 1:15; recordframe!(io); end
                    cell_label = 0
                    break
                end

                # update visuals:
                point[] = [u]
                push!(traj[], u)
                notify(traj)
                marker[] = state2marker[state]
                stateobs[] = state
                consecutiveobs[] = bsn_nfo.consecutive_match

                update_current_cell_color!(color_obs[n], bsn_nfo)

                recordframe!(io)
            end
        end
    end
end

animate_attractors_via_recurrences(mapper, u0s)