using DifferentialEquations
using Plots
using DataFramesPopulations exploitées
Prélèvements et effets Allee
Nous reprenons le modèle précédent sur l’effet Allee mais en prenant en compte des prélèvements externes avec un effort (taux) de prélèvement E : \dot x = r x \left(\frac{x}{\epsilon}-1\right)\left(1-\frac{x}{K}\right)-Ex. \tag{1}
La simulation de ce modèle pour différentes valeurs de E (par exemple E=0.2 ou E=0.85) ne présente aucune difficulté supplémentaire.
Nous allons maintenant nous intéresser à une situation où l’effort de prélèvement E varie au cours du temps entre une valeur soutenable E_s (par exemple 0.2), et une valeur excessive E_x (par exemple 0.85).
L’attendu théorique est que si les prélèvements sont maintenus à une valeur excessive E_x trop longtemps, la population disparait irrémédiablement même si les prélèvements sont par la suite ramenés à une valeur initialement soutenable E_s.
Prélèvements variables dans le temps
Définissons une fonction effort() dépendant du temps, qui renvoit :
- E_s~ si ~t<T_s
- E_x~ si ~t\in [T_s+T_x[
- à nouveau E_s~ si ~t\geq T_s+T_x
# paramètres
E_s = 0.2
E_x = 0.85
T_s = 10.0
T_x = 9.0
p_effort = [E_s, E_x, T_s, T_x]
function effort(t, p)
E_s, E_x, T_s, T_x = p
if t < T_s || t >= T_s + T_x
return E_s
elseif t >= T_s && t < T_s + T_x
return E_x
end
endRemarquez les opérateurs booléens “ou” || et “et” &&, ainsi que la structure du test if.
La fonction correspond bien à nos hypothèses :
Code
t2plot = 0: .1: 30
plot(
t2plot, # abscisses
[effort(t, p_effort) for t in t2plot]; # ordonnées
palette = :tab10,
linewidth = 2,
label = "\$E(t)\$",
xlabel = "temps",
ylabel = "\$E(t)\$",
title = "Effort de pêche \$E(t)\$",
margin = .5Plots.cm,
topmargin = 1Plots.cm,
)Remarquez la construction des ordonnées correspondant à la fonction effort via une compréhension de liste : [effort(t, p_effort) for t in t2plot].
Simulations en fonction du temps
Nous définissons les paramètres du modèle, la condition initiale et le temps d’intégration :
Code
x0 = 10.0
tspan = (0.0, 30.0)
tstep = 0.1
r = 1.0
K = 10.0
epsilon = 2.0
p_allee = [r, K, epsilon]Nous définissons le système dynamique comme précédemment, à la différence que nous prévoyons de surcharger l’argument p dans le problème d’intégration sous forme d’un vecteur comprennant :
- le vecteur de paramètres
p_alleeen première position - le vecteur de paramètres
p_efforten seconde position - et la fonction
efforten troisième position1
1 En Julia tout est passable en argument, des variables aux fonctions et plus.
p_evar = [p_allee, p_effort, effort]function allee_evar(u, p, t)
r, K, epsilon = p[1] # unpacking model parameters
p_effort = p[2] # unpacking fishing effort parameters
E = p[3] # unpacking fishing effort function
x = u # use x notation
return dx = r*x*(x/epsilon - 1)*(1 - x/K) - E(t, p_effort)*x
endL’intégration en elle-même suit le shéma vu précédemment, si ce n’est que l’argument de paramètres doit bien refléter ce qui est attendu par la fonction allee_evar(). La simulation en elle-même est effectuée avec une modification de la précision relative de l’intégration reltol = 1e-6, la précision par défaut n’étant pas suffisante ici.
prob_allee_evar = ODEProblem(
allee_evar,
x0,
tspan,
p_evar;
saveat = tstep,
)
sol_allee_evar = solve(prob_allee_evar; reltol = 1e-6)
sol_allee_evar = DataFrame(sol_allee_evar)
rename!(sol_allee_evar, :timestamp => :time, :value => :x)Ici nous avons imposé une tolérance plus précise au solver via le keyword argument reltol = 1e-6.
Finalement, nous pouvons représenter graphiquement la solution contre le temps. Ici, malgré la perturbation violente induite par la période de surexploitation, le pêcherie retrouve une situation soutenable après un retour à E=E_s.
Code
plot(
sol_allee_evar.time,
sol_allee_evar.x;
label = "\$x(t)\$",
linewidth = 2,
xlabel = "temps",
ylabel = "densité de population \$x(t)\$",
title = "Effort de pêche variant dans le temps",
palette = :tab10,
legend = :left,
margin = .5Plots.cm,
topmargin = 1Plots.cm,
)Lorsque la période de surexploitation de la population est trop longue (e.g. ici T_x=9.2), la population ne parvient pas à récupérer malgré le retour à un effort de prélèvement soutenable.
Code
# change parameter
T_x2 = 9.2
p_effort2 = [E_s, E_x, T_s, T_x2]
p_evar2 = [p_allee, p_effort2, effort]
# define new problem and integrate
prob_allee_evar2 = ODEProblem(
allee_evar,
x0,
tspan,
p_evar2,
saveat = tstep
)
sol_allee_evar2 = solve(prob_allee_evar2, reltol = 1e-6)
sol_allee_evar2 = DataFrame(sol_allee_evar2)
rename!(sol_allee_evar2, :timestamp => :time, :value => :x)
# plot
plot(
sol_allee_evar2.time, sol_allee_evar2.x,
label = "\$x(t)\$";
linewidth = 2,
xlabel = "temps",
ylabel = "densité de population \$x(t)\$",
title = "Effort de pêche variant dans le temps",
palette = :tab10,
legend = :left,
margin = .5Plots.cm,
topmargin = 1Plots.cm,
)┌ Warning: Using arrays or dicts to store parameters of different types can hurt performance.
│ Consider using tuples instead.
└ @ SciMLBase ~/.julia/packages/SciMLBase/XxTxA/src/performance_warnings.jl:32
Simulations dans l’espace (E, x)
Il s’agit ici de représenter dans l’espace (E, x) l’évolution conjointe de l’effort de pêche et de la densité de la population au cours du temps, afin de mieux comprendre le phénomène d’extinction. Dans cet objectif, on tracera aussi le lieu des équilibres de la population x^*(E) en fonction d’une valeur de E constante, i.e. : x^*(E) = 0, ce qui est équivalent à : E = r\left(\frac{x^*(E)}{\epsilon}-1\right)\left(1-\frac{x^*(E)}{K}\right).
Nous allons tracer les deux situations vues à la section précédente dans deux sous figures. Commençons par les lieux des équilibres, qui sont les mêmes dans les deux situations.
Définissons le lieu des équilibres positifs :
# parabole pour les equilibres positifs
function eeqpos(x, par = p_allee)
r, K, epsilon = par
return r*(x/epsilon -1)*(1-x/K)
endPuis traçons ce lieu dans le plan (E,x) (il y a deux branches une stable et une instable pour les équilibres positifs qui se rejoignent en x^* = (K+\epsilon)/2, nous les traçons séparément)
# vecteurs pour le tracé
e2plot = 0:.1:1
x2plot1 = epsilon:.02:(K+epsilon)/2
x2plot2 = (K+epsilon)/2:.02:K
# on définit des couleurs spécifiques depuis la palette :pal10
mygreen = palette(:tab10)[3]
myorange = palette(:tab10)[2]
myblue = palette(:tab10)[1]
myred = palette(:tab10)[4]
# plot
fig1 = plot(
e2plot,
zeros(length(e2plot));
color = mygreen,
linewidth = 2,
label = "équilibres stables",
ylabel = "densité de population \$x\$",
xlabel = "effort de pêche \$E\$",
legend = :left,
)
plot!(
fig1,
eeqpos.(x2plot1),
x2plot1;
color= myorange,
linewidth = 2,
label = "équilibre instable",
)
plot!(
fig1,
eeqpos.(x2plot2),
x2plot2;
color= mygreen,
linewidth = 2,
label ="",
)
display(fig1)Remarquez le broadcast (calcul terme à terme) sur les ordonnées des équilibres positifs en utilisant eeqpos.. On aurait pu procéder par compréhension de liste: [eeqpos(x) for x in x2plot2].
On prépare un graphique fig2 avec les mêmes éléments que dans fig1 :
fig2 = deepcopy(fig1) # on fait une copie complète de fig1Finalement, on complète fig1 et fig2 avec quelques annotations et les trajectoires calculées plus haut en fonction de l’effort de pêche variable au cours du temps, et on trace les résultats en deux sous-figures :
# annotations
annotate!(fig1, .35, 8.5, Plots.text("branche stable", 10, rotation=-28))
annotate!(fig1, .4, 2.7, Plots.text("branche instable", 10, rotation=28))
annotate!(fig2, .4, 2.7, Plots.text("branche instable", 10, rotation=28))
annotate!(fig2, .35, 8.5, Plots.text("branche stable", 10, rotation=-28))
# peche durable sur fig1
plot!(
fig1,
[effort(t, p_effort) for t in sol_allee_evar.time],
sol_allee_evar.x;
color = myblue,
linewidth = 2,
label = "trajectoire",
)
# surpeche sur fig2
plot!(
fig2,
[effort(t, p_effort2) for t in sol_allee_evar2.time], sol_allee_evar2.x;
color = myblue,
linewidth = 2,
label = "trajectoire",
)
# figure reprenant les deux sous figures fig1 et fig2
plot(
fig1,
fig2;
suptitle = "Effets Allee et prélèvements",
margin = .5Plots.cm,
topmargin = 1Plots.cm,
)La tordeuse du bourgeon de l’épinette
Modèle
Nous considérons le modèle de dynamique de populations suivant, inspiré de Ludwig, Jones, and Holling (1978) :
\dot x =rx\left(1-\frac{x}{K}\right) - \frac{\alpha x^2}{h^2+x^2}\ y, \tag{2}
avec x la densité de tordeuses et y la densité d’oiseaux. La croissance des tordeuses suit une loi logistique et la prédation des oiseaux une réponse fonctionnelle de type Holling III.
Simulations
On procède classiquement, en ajustant les valeurs de paramètres pour mettre en évidence les phénomènes dynamiques attendus :
# paramètres
r = 5.0 # natalité
K = 10.0 # mortalité
α = 1.0 # taux max de prédation, \alpha + TAB
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.02Puis on définit le modèle :
function tordeuse(u, p, t)
r, K, α, h, yc = p
x = u
return dx = r*x*(1 - x/K) - α*x^2/(h^2 + x^2)*yc
endComme pour le modèle avec effets Allee, on définit une fonction qui redéfinit le problème d’intégration à chaque fois, et simule et renvoit la solution pour pouvoir illustrer la bi-stabilité2 :
2 des méthodes alternative utilisant l’interface integrator de DifferentialEquations.jl ou la redéfinition du problème d’intégration via remake sont proposées en annexe
function int_tordeuse(x0, tspan = tspan, param = par_tordeuse)
prob_tordeuse = ODEProblem(
tordeuse, # modèle
x0, # condition initiale
tspan, # tspan
param; # paramètres
saveat = tstep,
)
sol_tordeuse = solve(prob_tordeuse)
sol_tordeuse = DataFrame(sol_tordeuse)
rename!(sol_tordeuse, :timestamp => :time, :value => :x)
endNous simulons le modèle depuis différentes conditions intiales, et traçons les résultats via une boucle for.
# conditions initiales
x0step = 1.35
x0vec = x0step:x0step:K
# custom color palette
init_cgrad = palette([:steelblue, :lightblue], length(x0vec))
# initialisation du graphique, équilibre nul
fig3 = plot(; # seulement des kwargs: on initialise le graphique
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!(
fig3,
int_tordeuse(x0).time,
int_tordeuse(x0).x,
linewidth = 2,
label = "",
)
end
display(fig3) # actually shows the plot fig3Equilibres
Les valeurs des équilibres positifs du modèle Equation 2 n’ont pas d’écriture mathématique simple. Nous allons calculer numériquement les racines du polynôme dont ils sont solution : r\left(1-\frac{x^*}{K}\right)\left(h^2+x^{*2}\right)-\alpha x^* y = 0 \tag{3}
Pour cela, on utilise le package Polynomials.jl :
using Polynomials
# définition du monôme X
X = Polynomial([0, 1])
# définition du polynôme
pol = r*(1-X/K)*(h^2 + X^2)-α*X*yc
# calcul des racines, réelles, positives et plus petites que K
eq_pos = roots(pol) # calcul des racines
eq_pos = real.(eq_pos[isreal.(eq_pos)]) # filtrage des racines réelles
eq_pos = eq_pos[(eq_pos .> 0) .& (eq_pos .<= K)] # filtrage des racines >0 et <K3-element Vector{Float64}:
0.20406511760131657
1.4717313636879934
8.324203518710693
A partir de la définition du monôme de degré 1, on écrit assez naturellement un polynôme et on en extrait les racines via roots.
La partie filtrage des racines réelles prend la partie réelle des racines qui sont rendues sous forme d’un type complexe (même si la partie imaginaire est nulle).
Remarquez les broadcasting divers, notamment .> et .& qui testent les conditions terme à terme sur le vecteur eq_pos.
Et on trace les différents équilibres :
t2plot = collect(tspan)
# initialisation du graphique, équilibre nul
plot!(
fig3,
t2plot,
zeros(length(t2plot));
linewidth = 2,
linestyle = :dash,
color = myorange,
palette = init_cgrad,
legend = :right,
label ="équilibres instables",
ylabel = "densité de population \$x(t)\$",
xlabel = "temps \$t\$",
margin = .5Plots.cm,
topmargin = 1Plots.cm,
)
# équilibres positifs, différents cas
if length(eq_pos) == 1
plot!(
fig3,
t2plot, ones(length(t2plot)).*eq_pos;
color = mygreen,
label ="équilibre stable",
)
elseif length(eq_pos) == 3
plot!(
fig3,
t2plot,
ones(length(t2plot)).*eq_pos[1];
lw=2,
linestyle = :dash,
color = mygreen,
label ="équilibres stables",
)
plot!(
fig3,
t2plot,
ones(length(t2plot)).*eq_pos[2];
lw=2,
linestyle = :dash,
color = myorange,
label = "",
)
plot!(
fig3,
t2plot,
ones(length(t2plot)).*eq_pos[3];
lw=2,
linestyle = :dash,
color = mygreen,
label = "",
)
else
println("Cette situation est non générique (2 équilibres positifs à la bifurcation)")
endDiagramme de bifurcations
Le début de cette partie est assez technique et a pour objectif principal de montrer ce qu’il est possible de faire en manipulant des expressions symboliques.
Finalement nous traçons dans le plan (y, x) le lieu des points d’équilibres en fonction de la taille de la population d’oiseaux:
y = \frac{r}{\alpha x^*}\left(1-\frac{x^*}{K}\right)(h^2+x^{*2}) \tag{4}
Comme nous l’avons vu en cours cette fonction est non monotone avec une branche décroissante, une branche croissante puis à nouveau une branche décroissante, le sens de variation de la branche déterminant la stabilité de l’équilibre correspondant.
Pour déterminer ces différentes branches et les représenter de différentes couleurs de façon à illustrer leur stabilité, nous allons dériver l’Equation 4 par rapport à x^* en utilisant le package Symbolics.jl et chercher les racines de la dérivée3.
3 si l’Equation 4 avait été un polynôme nous aurions utilisé les outils pour les polynômes, mais il s’agit d’une fraction rationnelle
using Symbolics
@variables X
D = Differential(X)
# on définit le lieu des équilibres selon l'équation ci-dessus
Y = r/(α*X)*(1-X/K)*(h^2+X^2)(5.0(1 - 0.1X)*(0.25 + X^2)) / X
Les racines de la dérivée, sont les racines du numérateur de la dérivée, donc on dérive:
# Dérivée de Y, distribuée et simplifiée
derY = simplify(expand_derivatives(D(Y)))(-1.25 + 5.0(X^2) - (X^3)) / (X^2)
et on récupère ce numérateur:
dnumerator = Symbolics.arguments(Symbolics.value(derY))[1]-1.25 + 5.0(X^2) - (X^3)
Comme ce numérateur est un polynôme, on peut utiliser le polynôme symbolique de Symbolics.jl pour regénérer un polynôme et utiliser la méthode roots() de Polynomials.jl4
4 il faut l’admettre, ce serait plus simple d’avoir directement une façon de trouver les racines d’un polynôme symbolique, mais ça ne semble pas implémenté pour l’instant (fin 2023)
# on récupère les coefficients X^k du polynôme
coefs_dict = Symbolics.value(dnumerator).dict
# on crée un dictionnaire rassemblant les coefficients du polynôme
poldict = Dict(Symbolics.degree(first(kv)) => kv[2] for kv in coefs_dict)
# on rajoute dans le dictionnaire le coefficient constant de dnumerator
poldict[0] = substitute(dnumerator, Dict( X=> 0))
# on définit le polynôme à partir du dictionnaire
dnumpoly = SparsePolynomial(poldict, :X)Finalement, on calcule les solutions (racines de la dérivée de y) en prenant soin de filtrer celles entre 0 et K.
# on calcule les solutions en filtrant les racines entre 0 et K via une fonction anonyme s-> K > s > 0
dyroots = filter(s -> K > s > 0, roots(dnumpoly))Ici nous utilisons une fonction anonyme s -> K > s > 0 qui renvoit true ou false selon que la condition est vérifiée par l’argument, et qui appliquée à roots(dnumpoly), “filtre” les valeurs vérifiant la condition.
On aurait pu faire un filtrage comme vu plus haut.
On calcule les branches du lieu des équilibres correspondantes, et on peut les tracer dans le plan (y,x) pour illustrer le diagramme de bifurcations.
# on calcule chacune des branches
xplot1 = 0.08:.01:dyroots[1]
xplot2 = dyroots[1]:.01:dyroots[2]
xplot3 = dyroots[2]:.01:K
yeq1 = [r*(1 - x/K)/(α*x)*(h^2 + x^2) for x in xplot1]
yeq2 = [r*(1 - x/K)/(α*x)*(h^2 + x^2) for x in xplot2]
yeq3 = [r*(1 - x/K)/(α*x)*(h^2 + x^2) for x in xplot3]
# diagramme de bifurcations
# branche stable 1 et initialisation
figbif = plot(
yeq1,
xplot1;
linewidth = 2,
color = mygreen,
label = "équilibres stables",
legend = :left,
xlabel = "population d'oiseaux \$y\$",
ylabel = "population de tordeuses \$x\$",
title = "Diagramme de bifurcations pour le modèle de tordeuses",
margin = .5Plots.cm,
topmargin = 1Plots.cm,
)
# branche instable
plot!(
figbif,
yeq2,
xplot2;
linewidth = 2,
color = myred,
label = "équilibres instables",
)
# branche stable 2
plot!(
figbif,
yeq3,
xplot3;
linewidth = 2,
color = mygreen,
label = "",
)
# équilibre nul
plot!(
figbif,
[0, maximum(yeq1)],
[0, 0];
color = myred,
lw = 2,
label = "",
)Nous verrons sur la page des populations en interactions le cas où la population d’oiseaux varie lentement au cours du temps.