Populations isolées

Le modèle de Malthus

Nous considérons le modèle proposé par Malthus (1798) : \dot x = (n-m)x, \tag{1} avec n le taux de natalité, et m le taux de mortalité.

Préliminaires

Pour simuler ce modèle, c’est à dire intégrer numériquement les solutions au problème de Cauchy correspondant à l’Equation 1 avec x(0)=x_0\geq0, nous allons utiliser les routines de DifferentialEquations.jl avec la méthode par défaut. Nous utiliserons aussi le package Plots.jl pour les représentations graphiques1.

  • 1 si vous n’avez jamais installé un package, il faudra commencer par le faire avant de pouvoir l’utiliser…

  • using DifferentialEquations
    using Plots

    Conditions initiales, paramètres et temps

    Nous définissions la condition initiale de la simulation :

    x0 = 0.1

    Nous définissons les paramètres, et les encapsulons dans un vecteur de paramètres :

    n = 3.0     # natalité
    m = 2.0     # mortalité
    par_malthus = [n, m] # packing in an array

    Enfin, les propriétés du temps d’intégration :

    tspan = (0.0, 10.0)
    tstep = 0.1

    Système dynamique

    On définit le modèle comme une fonction renvoyant la dérivée de x en fonction de l’état (u est la notation pour DifferentialEquations.jl), de paramètres (p), et du temps (t). L’ordre des arguments est important, c’est sous cette forme que les routines d’intégration attendent le modèle.

    function malthus(u, p, t)
        n, m = p                # unpacking
        x = u                # use x notation
        return dx = (n-m)x      # return derivative
    end
    Note

    L’ordre des arguments u, p, t est important pour le solver de DifferentialEquations.jl.

    Intégration

    On commence par définir le problème de Cauchy à intégrer, comme un ODEProblem avec arguments: le modèle, la condition initiale, les bornes d’intégration, les paramètres ainsi que l’option saveat = tstep permettant de récupérer la solution tout les tstep pas de temps (il y a plein d’options de sortie de l’intégration, dont même une fonction du temps !)

    prob_malthus = ODEProblem(
        malthus,          # modèle
        x0,               # condition initiale
        tspan,            # tspan
        par_malthus;      # paramètres
        saveat = tstep,   # keyword argument, option de sortie
    )

    On intègre le modèle via solve, défini par DifferentialEquations.jl pour des struct de type ODEProblem :

    sol_malthus = solve(prob_malthus)

    Le type de solution renvoyé par le solveur est assez complexe et comprend de nombreux champs informatifs sur le calcul. On peut accéder au temps de simulation via sol_malthus.t :

    first(sol_malthus.t, 3)
    3-element Vector{Float64}:
     0.0
     0.1
     0.2

    ainsi qu’aux valeurs de la variable x calculées le long du temps via sol_matlhus.u :

    first(sol_malthus.u, 3)
    3-element Vector{Float64}:
     0.1
     0.1105170918098962
     0.12214028021690636

    Même si ce n’est pas indispensable, il est possible de transformer la solution renvoyée facilement en DataFrame, qui peut permettre des manipulations plus faciles.

    using DataFrames
    
    sol_malthus = DataFrame(sol_malthus)
    rename!(sol_malthus, :timestamp => :time, :value => :x)
    Note

    remarquez la fonction rename! qui modifie en place le dataframe. Par convention les fonctions dont le nom finit par ! modifient leur argument en place.

    si bien que :

    first(sol_malthus, 3)
    3×2 DataFrame
    Row time x
    Float64 Float64
    1 0.0 0.1
    2 0.1 0.110517
    3 0.2 0.12214

    Représentation graphique

    On peut représenter graphiquement la simulation de la croissance de la population au cours du temps (ici via le dataframe).

    plot(
        sol_malthus.time,                   # abscisses
        sol_malthus.x;                      # ordonnées
        palette = :tab10,                   # palette de couleurs
        linewidth = 2,
        title = "\n Modèle de Malthus \$n=$n, m=$m\$",
        label = "population \$x\$",
        ylabel = "densité de population \$x(t)\$",
        xlabel = "temps \$t\$",
        margin = .5Plots.cm,
        topmargin = 1Plots.cm,
    )
    Note

    Les titres et autres chaînes de caractères peuvent utiliser des formules LaTeX via \$, e.g. "\$x\$".

    La notation "$m" accède à la valeur du paramètre m et la renvoit dans la chaine de caractères.

    Le modèle logistique

    Nous considérons ici le modèle “logistique” proposé par Verhulst (1838) :

    \dot x = r x \left(1-\frac{x}{K}\right), \tag{2} avec r le taux de croissance intrinsèque de la population et K la capacité de charge de l’environnement.

    Il n’y a pas de difficulté particulière par rapport aux simulations précedentes. On va réutiliser la condition initiale et les paramètres de temps définis précédemment.

    Il faut cependant définir les paramètres du modèle Equation 2 :

    Code
    r = 1.0      # natalité
    K = 10.0     # mortalité
    par_logistic = [r, K] # packing

    ainsi que le nouveau système dynamique :

    Code
    function logistic(u, p, t)
        r, K = p            # unpacking
        x = u            # use x notation
        return dx = r*x*(1-x/K)    # return derivative
    end

    et le problème de Cauchy correspondant :

    Code
    prob_logistic = ODEProblem(
        logistic,         # modèle
        x0,               # condition initiale
        tspan,            # tspan
        par_logistic;     # paramètres
        saveat = tstep,   # option de sortie
    )

    On simule et on transforme la solution en dataframe :

    Code
    sol_logistic = solve(prob_logistic)
    
    sol_logistic = DataFrame(sol_logistic)
    rename!(sol_logistic, :timestamp => :time, :value => :x)

    On trace la solution, en rajoutant les équilibres stable (x=K) et instable (x=0) :

    # solution
    plot(
        sol_logistic.time,
        sol_logistic.x;
        palette = :tab10,
        linewidth = 2,
        title = "\n Modèle logistique \$r=$r, K=$K\$",
        label = "population \$x\$",
        legend = :right,
        ylabel = "densité de population \$x(t)\$",
        xlabel = "temps \$t\$",
        margin = .5Plots.cm,
        topmargin = 1Plots.cm,
    )
    
    # équilibre 0
    plot!(
        sol_logistic.time,
        zeros(length(sol_logistic.time));
        color = "red",
        linewidth = 2,
        linestyle = :dash,
        linealpha = .5,
        label = "équilibre instable",
    )
    
    # équilibre K
    plot!(
        sol_logistic.time,
        ones(length(sol_logistic.time)).*K;
        color = "green",
        linewidth = 2,
        linestyle = :dash,
        linealpha = .5,
        label = "équilibre stable",
    )
    Note

    Remarquez l’utilisation de plot! qui modifie en place la première figure en rajoutant des éléments (ici les équilibres et leurs labels).

    Effets Allee

    On s’intéresse à un modèle de dynamique de population avec “effets Allee forts”, souvent attribué à Gruntfest, Arditi, and Dombrovsky (1997)2 :

  • 2 mais de nombreuses variations de cette forme polynomiale existent dans la littérature depuis Bazykin (1985)

  • \dot x = r x \left(\frac{x}{\epsilon}-1\right)\left(1-\frac{x}{K}\right), \tag{3} avec r le taux de croissance intrinsèque de la population (par analogie avec la logistique), K la capacité de charge de l’environnement et \epsilon le seuil en dessous duquel la population n’est pas viable (‘seuil de Allee’).

    Nous souhaitons illustrer la bistabilité caractéristique du comportement de ce modèle à partir de la simulation depuis plusieurs conditions initiales.

    Commençons par définir le modèle et les paramètres (en conservant les r et K précédents):

    Code
    epsilon = 2                 # seuil de Allee
    par_allee = [r, K, epsilon] # packing
    tspan_allee = (0.0, 3.0)
    
    function allee(u, p, t)
        r, K, epsilon = p        # unpacking
        x = u                 # use x notation
        return dx = r*x*(x/epsilon - 1)*(1 - x/K)     # derivative
    end

    Nous définissons une fonction qui réalise une simulation en un seul appel depuis une condition initiale x_0 :

    function int_allee(x0, tspan = tspan_allee, param = par_allee)
        prob_allee = ODEProblem(
            allee,            # modèle
            x0,               # condition initiale
            tspan,            # tspan
            param;            # paramètres
            saveat = tstep,   # option de sortie
            )
    
        sol_allee = solve(prob_allee)
        sol_allee = DataFrame(sol_allee)
        rename!(sol_allee, :timestamp => :time, :value => :x)
    
        return sol_allee
    end
    Note

    La fonction int_allee comporte un nombre d’arguments variable: int_allee(x0) renverra la solution avec les arguments tspan et param à leurs valeurs par défaut (ce sont des vararg); c’est le même appel que int_allee(x0, tspan_allee, par_allee).

    Nous définissons un vecteur de conditions initiales différentes :

    x0step = 1.35
    x0vec = x0step:x0step:K     # range de valeur

    Finalement on réalise la figure. La stratégie diffère un peu de ce qui a été vu ci-dessus. Nous commençons par initier un graphique fig, et faisons une boucle for pour tracer chacune des simulations correspondant aux différentes conditions initiales3 :

  • 3 la palette de couleur est accessoire, juste pour l’esthétique

  • # custom color palette
    init_cgrad = palette([:steelblue, :lightblue], length(x0vec))
    
    # initialisation du graphique
    fig = plot(;          # on initie uniquement le graphique
        palette = init_cgrad,
        title = "\n Modèle à effets Allee \$r=$r, K=$K\$, \$ϵ=$epsilon\$",
        legend = :right,
        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         # x0 parcourt l'array x0vec
        plot!(
            fig,              # on modifie fig
            int_allee(x0).time, # abscisses
            int_allee(x0).x;    # ordonnées
            linewidth = 2,      # keyword arguments après ;
            label = "",
        )
    end
    
    # équilibre 0
    plot!(
        fig,
        int_allee(0).time,
        zeros(length(int_allee(0).time));
        color = "green",
        linewidth = 2,
        linestyle = :dash,
        linealpha = .5,
        label = "équilibre stable",
    )
    
    # équilibre epsilon
    plot!(
        fig,
        int_allee(0).time,
        ones(length(int_allee(0).time)).*epsilon;
        color = "red",
        linewidth = 2,
        linestyle = :dash,
        linealpha = .5,
        label = "équilibre instable",
    )
    
    # équilibre K
    plot!(
        fig,
        int_allee(0).time,
        ones(length(int_allee(0).time)).*K;
        color = "green",
        linewidth = 2,
        linestyle = :dash,
        linealpha = .5,
        label = "",
    )
    
    display(fig)      # actually shows the plot P


    On peut sauvegarder la figure dans différents formats (e.g. png, pdf) :

    savefig(fig, "dyn_allee.pdf")
    savefig(fig, "dyn_allee.png")

    Passons aux populations exploitées.

    References

    Bazykin, A. D. 1985. Mathematical Biophysics of Interacting Populations. Nauka, Moscow (in Russian).
    Gruntfest, Y., R. Arditi, and Y. Dombrovsky. 1997. “A Fragmented Population in a Varying Environment.” Journal of Theoretical Biology 185: 539–47.
    Malthus, T. R. 1798. An Essay on the Principle of Population. J. Johnson.
    Verhulst, P. F. 1838. “Notice Sur La Loi Que La Population Poursuit Dans Son Accroissement.” Correspondance Mathématique Et Physique 10: 113–21.

    Reuse