Code
%reset -f
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
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 ramenés à une valeur initialement soutenable E_s.
%reset -f
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
Définissons la fonction Evar()
, qui renvoit E_s si t est plus petit que T_s ou plus grand que T_s+T_x, et E_x sinon.
Définissons d’abord les paramètres de la fonction et encapsulons les :
= 0.2
E_s = 0.85
E_x = 10.0
T_s = 9.0
T_x
= np.array([E_s, E_x, T_s, T_x]) params_Evar
Définissons la fonction Evar()
:
def Evar(t, params):
= params
E_s, E_x, T_s, T_x if t <= T_s or t > T_s+T_x:
return E_s
else:
return E_x
Définissons les autres paramètres du modèle et l’intervalle de temps considéré
## paramètres
= 1.0 # taux de croissance intrinsèque
r = 2.0 # seuil de Allee
epsilon = 10.0 # capacité de charge
K = np.array([r, K, epsilon])
params_allee
## tspan
= 0.0
t_0 = 30.0
t_fin = 0.01
pas_t = np.arange(t_0, t_fin, pas_t) tspan
La fonction se comporte bien comme attendu (Figure 1).
= plt.subplots(1, 1, figsize=(6,4))
fig, ax 'Evar() en fonction du temps',
fig.suptitle(='top', fontsize='14')
va
for t in tspan], label = "Evar(t)")
ax.plot(tspan, [Evar(t, params_Evar)
## axes / légendes / grille
='10')
ax.legend(fontsize'Temps $t$', fontsize='12')
ax.set_xlabel('Effort de prélèvement', fontsize='12')
ax.set_ylabel( ax.grid()
Evar()
en fonction du tempsOn notera l’ultilisation d’une compréhension de liste pour le tracé de Evar()
. En effet, la fonction avec son test if
travaille sur un argument t
qui est un scalaire et renvoit un scalaire ce qui ne permet pas d’appeler Evar(tspan)
directement.
Définissons le système dynamique comme usuellement :
def model_alleePrelev(etat, t, params, paramsEv):
= etat # on recupere l'etat
x = params # on récupère les paramètres
r, K, epsilon = r*x*(x/epsilon-1)*(1-x/K) - Evar(t, paramsEv)*x # la derivee
xdot return xdot
A la différente de précédemment, le modèle est ici non-autonome (la dérivée dépend du temps en plus de l’état) via la fonction Evar(t)
.
Intégrons :
## condition initiale
= K
x0
## intégration
= odeint(model_alleePrelev,
int_alleePrelev
x0,
tspan,=(params_allee, params_Evar),
args=pas_t) hmax
Comme la fonction Evar()
a deux arguments de paramètres, on les passe passe à la suite comme tuple
à args
.
Représentation graphique, lorsque la durée des prélèvements excessifs n’est pas trop longue la population récupère de la pression de prélèvement trop intense :
= plt.subplots(1, 1)
fig1, ax1 'Simulation du modèle avec effets Allee'\
fig1.suptitle(' forts et prélèvements; $T_x={}$'.format(T_x),
='top', fontsize='14')
va
## simulation
ax1.plot(tspan, int_alleePrelev,= "$x(t)$")
label
## axes / légendes / grille
='10')
ax1.legend(fontsize'Temps $t$', fontsize='12')
ax1.set_xlabel('Densité de population $x$', fontsize='12')
ax1.set_ylabel( ax1.grid()
Par contre, si la durée des prélèvements est trop longue, la population est conduite à l’extinction.
Modification du paramètre T_x, réintégration, représentation :
## redefinition de T_x et des paramètres
= 9.2
T_x2 = np.array([E_s, E_x, T_s, T_x2])
params_Evar2
## intégration
= odeint(model_alleePrelev,
int_alleePrelev2
x0,
tspan,=(params_allee, params_Evar2),
args=pas_t)
hmax
= plt.subplots(1, 1)
fig2, ax2 'Simulation du modèle avec effets Allee forts '\
fig2.suptitle('et prélèvements; $T_x={}$'.format(T_x),
='top', fontsize='14')
va
## simulation
ax2.plot(tspan, int_alleePrelev2,= "$x(t)$")
label
## axes / légendes / grille
='10')
ax2.legend(fontsize'Temps $t$', fontsize='12')
ax2.set_xlabel('Densité de population $x$', fontsize='12')
ax2.set_ylabel( ax2.grid()
Une représentation complémentaire dans l’expace (E,x^*), permet de bien saisir le phénomène de catastrophe lié à la présence d’une bifurcation pli.
Nous commençons par calculer le lieu des équilibres en fonction de E puis traçons les deux cas simulés plus haut pour deux valeurs de T_x.
Les équilibres sont définis par :
x^*=0 est toujours équilibre
les équilibres x^* positifs vérifient : E = r\left(\frac{x^*}{K_a}-1\right)\left(1-\frac{x^*}{K}\right)
Traçons tout d’abord les lieux des équilibres :
## définition de vecteurs annexes E_plot et x_plot
= np.arange(0, 1, 0.01)
E_plot = np.arange(epsilon, (K+epsilon)/2, 0.01)
x_plot_l = np.arange((K+epsilon)/2, K, 0.01)
x_plot_L
## définition des valeurs de E correspondant aux équilibres positifs
= r*(x_plot_l/epsilon-1)*(1-x_plot_l/K)
E_eq_l = r*(x_plot_L/epsilon-1)*(1-x_plot_L/K)
E_eq_L
## création d'une figure, et de deux subplots (ax6, ax7)
= plt.subplots(1, 2, figsize=(9, 6))
fig2, (ax3, ax4)
## titre de la figure
'Effet Allee forts et prèlevements\n '\
fig2.suptitle('$T_x$ = {} (gauche), $T_x$ = {} (droite)'
format(T_x, T_x2), va='top', fontsize='14')
.
## premier subplot
## tracé du lieu des équilibres
= 'C2')
ax3.plot(E_plot, np.zeros_like(E_plot), color ='C2', label='équilibre stable')
ax3.plot(E_eq_L, x_plot_L, color='C3', label='équilibre instable')
ax3.plot(E_eq_l, x_plot_l, color
## labellisation des axes
'effort de prélèvement $E$', fontsize='12')
ax3.set_xlabel('densité de population $x$', fontsize='12')
ax3.set_ylabel(
ax3.grid()
## second subplot
## tracé du lieu des équilibres
= 'C2')
ax4.plot(E_plot, np.zeros_like(E_plot), color ='C2', label='équilibre stable')
ax4.plot(E_eq_L, x_plot_L, color='C3', label='équilibre instable')
ax4.plot(E_eq_l, x_plot_l, color
## labellisation des axes
'effort de prélèvement $E$', fontsize='12')
ax4.set_xlabel(
ax4.grid()
plt.close(fig2)
On complète la figure avec l’ajout des trajectoires en fonction de E et affichage de la figure complète :
## premier subplot
for t in tspan], int_alleePrelev,
ax3.plot([Evar(t, params_Evar) = 'C0', label = 'trajectoire')
color ='10', loc = 'lower right',
ax3.legend(fontsize=(0.5, 0.05, 0.5, 0.5))
bbox_to_anchor
## second subplot
for t in tspan], int_alleePrelev2,
ax4.plot([Evar(t, params_Evar2) = 'C0', label = 'trajectoire')
color ='10', loc = 'lower right',
ax4.legend(fontsize=(0.5, 0.05, 0.5, 0.5))
bbox_to_anchor
display(fig2)
Notons le remplissage de la figure en deux cellules Jupyter successives, avec un seul affichage grace à plt.close()
et display()
.
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}
On procède classiquement :
## on nettoie l'espace de travail et on reload les modules
%reset -f
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
## tspan
= 0
t_0 = 3.0
t_fin = 0.01
pas_t = np.arange(t_0, t_fin, pas_t)
tspan
## paramètres du modèle
= 5.0
r = 10.0
K = 1.0
alpha = 0.5
h = 7.0
y_c = np.array([r, K, alpha, h, y_c])
params_tordeuse_yc
## définition du modèle tordeuse avec pop oiseaux constante
def model_tordeuse_yc(etat, t, params):
= etat # on recupere l'etat
x = params # on récupère les paramètres
r, K, alpha, h, y_c = r*x*(1-x/K) - alpha*(x**2)*y_c/((h**2)+(x**2)) # la derivee
xdot return xdot # on renvoie la derivée calculée
Pour mettre en valeur la multi-stabilité, nous intégrons depuis plusieurs conditions initiales, comme dans la section sur l’effet Allee, et définissons une fonction pour faire une boucle :
def int_tordeuse(x0, tspan=tspan, params=params_tordeuse_yc):
= odeint(
sim_tordeuse
model_tordeuse_yc,
x0,
tspan, =(params,),
args=pas_t)
hmaxreturn sim_tordeuse
## multi-conditions initiales
= 1.35
x0_step = np.arange(x0_step, K, x0_step) x0_arr
La représentation graphique suit le modèle de la section que nous complèterons ensuite avec les équilibres positifs.
= plt.subplots(1, 1)
fig, ax 'Simulation du modèle de tordeuse avec population'\
fig.suptitle(' d\'oiseaux constante',
='top', fontsize='14')
va
## redéfinition du cycle des couleurs pour un dégradé de bleu
= plt.cm.Blues(np.linspace(.8, .3, x0_arr.shape[0]))
colorTordeuse = colorTordeuse)
ax.set_prop_cycle(color
## simulations
0]),
ax.plot(tspan, int_tordeuse(x0_arr[= "$x(t)$")
label for x0 in x0_arr[1:]: # x0 parcour x0_arr
ax.plot(tspan, int_tordeuse(x0))
## équilibres
ax.plot(tspan, np.zeros_like(tspan),= 'C3',
color = 'dashed',
linestyle = "équilibre instable")
label
## axes / légendes / grille
'Temps $t$', fontsize='12')
ax.set_xlabel('Densité de population $x$', fontsize='12')
ax.set_ylabel(
ax.grid() plt.close(fig)
Les équilibres positifs sont solutions d’un polynôme de degré 3 :
r\left(1-\frac{x^*}{K}\right)\left(h^2+x^{*2}\right)-\alpha x^* y = 0,
\tag{3} et ils n’ont pas de forme facile à expliciter analytiquement. Nous allons les calculer numériquement via les méthodes pour polynômes de numpy
pour pouvoir les intégrer à la figure précédente.
## monôme de degré 1
= np.polynomial.Polynomial([0, 1])
z
## polynome dont les racines sont les x* > 0
= r*(1-z/K)*(h**2+z**2)-alpha*z*y_c
pol
## on calcule les racines et on récupère seulement les réelles, > 0 et < K
= pol.roots()[(np.isreal(pol.roots()))
eq_pos * (pol.roots() < K) * (pol.roots() > 0)]
pol.roots()
renvoit un array
avec toutes les racines de l’Equation 3. Les seules qui font sens dans notre problème sont les racines réelles, positives, et plus petites que K. Nous utilisons un masque booléen sur l’array
renvoyé pour filtrer seulement les racines vérifiant ces conditions.
Complétons maintenant la figure :
## d'après l'étude mathématique, on s'attend à 1 ou 3 équilibres positifs
if eq_pos.size == 1:
*eq_pos,
ax.plot(tspan, np.ones_like(tspan)='C2', label ="équilibre stable")
color
elif eq_pos.size == 3:
*eq_pos[0],
ax.plot(tspan, np.ones_like(tspan)= 'dashed', color='C2', label='équilibre stable')
linestyle *eq_pos[1],
ax.plot(tspan, np.ones_like(tspan)= 'dashed', color='C3')
linestyle *eq_pos[2],
ax.plot(tspan, np.ones_like(tspan)= 'dashed', color='C2')
linestyle
='10')
ax.legend(fontsize display(fig)
Pour le diagramme de bifurcation dans l’espace (y, x), on commence par calculer les différentes branches du lieu des points d’équilibre positifs
## équilibres positifs
## on récupère les extrema du lieu des équilibres positifs
from sympy import Symbol, solve, Derivative, re
= Symbol('x')
x ## lieu des équilibres
= r*(1-x/K)*(h**2+x**2)/(alpha*x)
yfunc ## dérivée
= Derivative(yfunc, x).doit()
dyfunc ## points critiques positifs.
## solve() renvoit ici des complexes avec partie Im. presque nulles
= [re(root) for root in solve(dyfunc) if re(root)>0]
crit_points
## on calcule chacune des branches
= np.arange(0.01, crit_points[0], 0.01)
xplot1 = np.arange(crit_points[0], crit_points[1], 0.01)
xplot2 = np.arange(crit_points[1], K, 0.01)
xplot3 = r*(1-xplot1/K) / (alpha*xplot1)*(h**2+xplot1**2)
yeq1 = r*(1-xplot2/K) / (alpha*xplot2)*(h**2+xplot2**2)
yeq2 = r*(1-xplot3/K) / (alpha*xplot3)*(h**2+xplot3**2) yeq3
Nous avons utilisé ici le module sympy
pour calculer les points critiques du lieu des points d’équilibre positifs. Notons la compréhension de liste pour générer les points critiques.
= plt.subplots(1, 1)
fig2, ax2
'Diagramme de bifurcation pour le modèle'\
fig2.suptitle(' de tordeuse',
='top', fontsize='14')
va
## équilibre nul
= np.arange(0, 15, 0.1)
yplot = "C3", label = "équilibre instable")
ax2.plot(yplot, np.zeros_like(yplot), color
## équilibres positifs
='C2', label='équilibre stable')
ax2.plot(yeq1, xplot1, color='C3')
ax2.plot(yeq2, xplot2, color='C2')
ax2.plot(yeq3, xplot3, color
## bornes abscisses
= -.50, right = 15)
ax2.set_xlim(left
'Population de tordeuses $x$', fontsize='12')
ax2.set_ylabel('Population d\'oiseaux $y$', fontsize='12')
ax2.set_xlabel(='center left')
ax2.legend(loc ax2.grid()
Le changement principal ici est la dimension du modèle (dimension 2) : les tailles de populations de tordeuses x et d’oiseaux y varient toutes deux au cours du temps en s’influençant l’une l’autre, avec une population d’oiseaux qui varie lentement (d’où le paramètre \varepsilon).
Le modèle prend la forme :
\left\{
\begin{array}{l}
\displaystyle \dot x = rx\left(1-\frac{x}{K}\right) - \frac{\alpha x^2}{h^2+x^2}\ y \\[.3cm]
\displaystyle \dot y = \varepsilon \left(\frac{n \alpha x^2}{h^2+x^2}\ y -m y\right)
\end{array}
\right.
\tag{4}
La fonction odeint()
intègre parfaitement ce type de système, il faut juste écrire correctement le système d’équations différentielle à passer à la fonction.
def model_tordeuse_yvar(etat, t, params):
= etat # recupere les variables d'etat
x, y = params # recupere les parametres
r, K, alpha, h, epsilon, n, m = [r*x*(1 - x/K) - alpha*x**2/(h**2+x**2)*y, # dot x
etatdot *(n*alpha*x**2/(h**2+x**2)-m)*y] # dot y
epsilon
return etatdot # renvoit la derivee
Définissons les conditions initiales et paramètres :
## densités initiales de populations
= 1
x0 = 2.5
y0 = np.array([x0, y0]) # encapsulation
etat0_tordeuse_yvar
## tspan
= 400.0
tfin_yvar = np.arange(0.0, tfin_yvar, pas_t)
tspan_yvar
## paramètres spécifiques à la population d'oiseaux
= 0.01
epsilon = 5.0
n = 3.0
m = np.array([r, K, alpha, h, epsilon, n, m]) params_tordeuse_yvar
Intégrons (l’appel à odeint()
est similaire) :
= odeint(
int_tordeuse_yvar
model_tordeuse_yvar,
etat0_tordeuse_yvar,
tspan_yvar, =(params_tordeuse_yvar,),
args=pas_t) hmax
L’intégration renvoit un array
dont la première colonne est la simulation de la population x et la seconde celle de la population y.
5,] int_tordeuse_yvar[:
array([[1. , 2.5 ],
[1.02540404, 2.50025497],
[1.05163666, 2.50051974],
[1.07872903, 2.50079407],
[1.10671295, 2.50107772]])
Représentation graphique contre le temps :
= plt.subplots(1, 1)
fig3, ax3 'Simulation du modèle de tordeuse'\
fig3.suptitle(' avec population d\'oiseaux variable',
='top', fontsize='14')
va
## simulations
0],
ax3.plot(tspan_yvar, int_tordeuse_yvar[:,= 'C0',
color = "$x(t)$")
label
## axes / légendes / grille
'Temps $t$', fontsize='12')
ax3.set_xlabel('Densité de population $x(t)$', fontsize='12')
ax3.set_ylabel(
ax3.legend() ax3.grid()
Notons l’appel à toutes les lignes de la première colonne de int_tordeuse_var
pour récupérer la simulation de la population de tordeuses.
Dans l’espace (y, x), on voit bien l’enchainement des catastrophes et le phénomène de ‘bifurcations dynamiques’ :
'Diagramme de bifurcation pour le modèle de tordeuse',
fig2.suptitle(='top', fontsize='14')
va
1], int_tordeuse_yvar[:,0], label='simulation')
ax2.plot(int_tordeuse_yvar[:,
ax2.legend() display(fig2)
On a tracé ici le lieu de la simulation population de tordeuses en fonction de la population d’oiseaux, le temps étant implicite.
La suite sur les populations en interactions par ici.