Simulations multiples

Nous abordons ici brièvement les méthodes integrator et remake de DifferentialEquations.jl pour les simulations multiples.

Interface Integrator pour simulations multiples

Lors de simulations multiples1, la stratégie choisie a été de redéfinir un nouveau problème d’intégration ODEProblem pour chaque condition initiale. Cette procédure est a priori peu efficace, et DifferentialEquations.jl permet de modifier un problème d’intégration via la Integrator Interface.

  • 1 par exemple pour plusieurs conditions initiales pour le modèle avec effets Allee ou celui de la tordeuse du bourgeon de l’épinette

  • Répliquons la figure des dynamiques de la tordeuse du bourgeon de l’épinette avec cette méthode. On commence par définir les paramètres et le modèle:

    Code
    using DifferentialEquations
    using Plots
    using DataFrames
    
    # paramètres
    r = 5.0      # natalité
    K = 10.0     # mortalité
    α = 1.0      # taux max de prédation
    h = 0.5      # constante de demi-saturation
    yc = 7.0     # densité de prédateurs
    
    par_tordeuse = [r, K, α, h, yc]
    
    # temps d'intégration
    tspan = (0.0, 3.0)
    tstep = 0.02
    
    # condition initale
    x0step = 1.35
    
    # modèle
    function tordeuse(u, p, t)
        r, K, α, h, yc = p
        x = u[1]
        return dx = r*x*(1 - x/K) - α*x^2/(h^2 + x^2)*yc
    end

    Nous définissons ensuite le problème d’intégration ainsi que l’intégrateur integrator:

    prob_tordeuse = ODEProblem(
        tordeuse,         # modèle
        x0step,           # condition initiale
        tspan,            # tspan
        par_tordeuse;     # paramètres
        saveat = tstep,
    )   # option de sortie
    
    integrator = init(prob_tordeuse, Tsit5())  # integrator requires integ algorithm

    Pour une utilisation dans une boucle pour simuler les trajectoires depuis différentes conditions initiales, nous définissons une fonction qui réinitialise l’intégrateur à la nouvelle condition, effectue la simulation (et transforme la solution en dataframe).

    function int_tordeuse(x0, integrator)
        reinit!(integrator, x0)             # la clef de l'interface est ici
    
        sol_tordeuse = solve!(integrator)   # et là
        sol_tordeuse = DataFrame(sol_tordeuse)
        rename!(sol_tordeuse, :timestamp => :time, :value => :x)
    
        return sol_tordeuse
    end

    On construit ensuite le graphique et on fait une boucle pour intégrer et tracer les résultats:

    # conditions initiales
    x0vec = x0step:x0step:K
    
    # custom color palette
    init_cgrad = palette([:steelblue, :lightblue], length(x0vec))
    
    # initialisation du graphique, équilibre nul
    fig = plot(;
        palette = init_cgrad,
        legend = :right,
        label = "équilibres instables",
        title = "Tordeuse du bourgeon de l\'épinette",
        ylabel = "densité de population \$x(t)\$",
        xlabel = "temps \$t\$",
        margin = .5Plots.cm,
        topmargin = 1Plots.cm,
    )
    
    # boucle de plot avec intégration pour differentes conditions initiales
    for x0 in x0vec
        plot!(
            fig,
            int_tordeuse(x0, integrator).time,
            int_tordeuse(x0, integrator).x,
            linewidth = 2,
            label = "",
        )
    end
    
    display(fig)      # actually shows the plot P


    On peut compléter la figure comme vu précédemment, mais cela n’a pas plus d’intéret ici.

    Commentaires

    Le code de la fonction int_tordeuse est plus simple avec l’integrator interface qu’en redéfinissant le problème d’intégration à chaque fois. Néanmoins, sur un problème avec x0step= 0.1 (soit ~100 trajectoires calculées), le gain en temps de calcul de cette méthode est marginal : 123 ms contre 128 ms avec la méthode précédente (test avec package BenchmarkTools.jl).

    Le gain en mémoire est plus sensible : 4.28 MiB contre 7.26 MiB, mais finalement marginal aussi sur des machines modernes pour de si petits problèmes d’intégration.

    Redéfinition du problème d’intégration via remake

    Une autre approche plutôt que de redéfinir un nouveau problème d’intégration à chaque fois, est de modifier le problème d’intégration via la fonction remake.

    On définit une fonction qui prend pour arguments la condition initiale et un problème d’intégration, et redéfinit le problème d’intégration depuis cette condition initiale :

    function int_tordeuse2(x0, prob)
        prob = remake(prob, u0 = x0)       # redéfinition du problème d'intégration
    
        sol_tordeuse = solve(prob)
        sol_tordeuse = DataFrame(sol_tordeuse)
        rename!(sol_tordeuse, :timestamp => :time, :value => :x)
    end
    
    # initialisation du graphique
    fig2 = plot(;
        palette = init_cgrad,
        legend = :right,
        label = "équilibres instables",
        title = "Tordeuse du bourgeon de l\'épinette",
        ylabel = "densité de population \$x(t)\$",
        xlabel = "temps \$t\$",
        margin = .5Plots.cm,
        topmargin = 1Plots.cm,
    )
    
    for x0 in x0vec
        plot!(
            fig2,
            int_tordeuse2(x0, prob_tordeuse).time,
            int_tordeuse2(x0, prob_tordeuse).x;
            linewidth = 2,
            label = "",
        )
    end
    
    display(fig2)


    Cette méthode est plus rapide que la redéfinition de problème d’intégration ou l’utilisation de l’integrator interface (113 ms dans les conditions vues plus haut) et intermédiaire en termes de mémoire utilisée (6.39 MiB). Le code est aussi simple que pour l’integrator interface.

    Par ailleurs la même approche via remake peut être utilisée pour redéfinir un problème d’intégration en modifiant les paramètres plutôt que la condition initiale, par exemple pour calculer un diagramme de bifurcations par force brute comme pour le modèle de Rosenzweig MacArthur. C’est visiblement la bonne solution algorithmique.