Simulation de feu de forêt¶
L’essentiel de l’activité¶
L’objectif de l’activité est de construire une interface graphique en Tkinter simulant la propagation d’un feu de forêt. Dans la version simple, cela donnerait ceci :
Une animation véritablement interactive est proposée en fin d’activité.
La forêt est en fait plutôt une parcelle. Elle est représentée sous la forme d’une grille carrée de taille \(\mathtt{n\times n}\) contenant \(\mathtt{n^2}\) cellules. Ces dernières sont d’un des quatre types suivants :
Type de cellule | Couleur de la cellule | Code |
Sol nu | ivoire | 0 |
Arbre vivant | verte | 1 |
Arbre qui brûle | rouge | 2 |
Arbre en cendres | grise | 3 |
Si \(\mathtt{N}\) est le nombre d’arbres, on notera \(\mathtt{p=N/(n\times n)}\) la proportion d’arbres (ou encore la densité) dans la parcelle. En pratique, on se donnera \(\mathtt{n}\) ainsi qu’une proportion, par exemple \(\mathtt{p=3/4}\) et on en déduira la population \(\mathtt{N}\) d’arbres de la parcelle.
L’évolution du feu de forêt dans l’interface graphique ne sera que le reflet de l’évolution de la grille dont les cellules changent d’état. Chaque cellule passe d’un état « courant » à un état « suivant » :
- si la cellule est un arbre en feu (code 2 ci-dessus), à l’état suivant, elle devient une cellule d’un arbre en cendres (code 3),
- si la cellule est un arbre en feu, et qu’une de ses voisines
v
dans les quatre directions est un arbre vivant (code 1), alorsv
devient une cellule d’un arbre en feu à l’état suivant.
Il s’agit donc de coder une succession de grilles jusqu’à ce qu’il n’y ait plus aucune cellule en feu dans la grille. Et puis de faire évoluer cette succession de grilles dans un canevas Tkinter.
Structure de données¶
Dans cette partie, on code la simulation d’abord sans tenir compte de la partie graphique. Il y aura deux étapes :
on écrit une fonction
random_forest(n, p)
qui renvoie une forêt aléatoire ayant une densité donnéep
; cette forêt sera représentée par un tableau 2D de taille \(\mathtt{n\times n}\), chaque élément valant un entier 0, 1, 2 ou 3 ;on écrit une fonction
update_states(states)
capable de mettre à jour la grille selon l’état de la forêt :- les cellules en feu deviendront des cendres,
- les cellules vertes (un arbre) voisines d’une cellule rouge (feu) deviendront elle-mêmes rouges.
Jemploierai parfois le terme de matrice des états pour désigner la grille states
.
Voici une fonction qui génère une forêt aléatoire :
foret_alea.py
1 2 3 4 5 6 7 8 9 10 11 12 13 | from random import sample
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
states = random_forest(0.6, 10)
print('\n'.join(' '.join(map(str, L)) for L in states))
|
0 0 0 0 0 1 0 1 1 1
0 1 0 1 0 0 1 1 1 1
1 1 1 0 1 0 0 0 0 1
0 1 0 1 1 1 1 1 0 1
1 0 0 1 1 0 1 1 0 0
1 1 1 1 1 1 1 1 0 0
1 0 1 1 1 1 1 0 1 0
1 1 1 1 1 1 0 1 1 0
1 0 1 0 0 1 0 1 1 0
0 0 0 1 1 1 0 1 0 1
- Ligne 12 : on a généré une forêt 10 x 10 avec une proportion de
60 %
d’arbres et on l’affiche (lignes 13 et 14-23) - Ligne 5 : le nombre d’arbres est \(\mathtt{ntrees=n^2\times p}\).
- Ligne 6 : avec la fonction
sample
du module standardrandom
, on tire au hasard l’échantillon de \(\mathtt{ntrees}\) arbres parmi les \(\mathtt{n^2}\) emplacements possibles. - Ligne 7 :
states
est la grille représentant l’état de chaque cellule de la forêt, initialement sans arbre (uniquement du sol, code 0) ; on place les arbres tirés au hasard ligne 6. - Lignes 8-9 et les lignes de sortie : on obtient donc une matrice formée uniquement de 0 et de 1.
Passons à la fonction de mise à jour updates_states(states)
. Cette fonction va examiner l’état de chaque cellule de la grille courante states
. En particulier, si la cellule correspond à des flammes, la fonction va regarder s’il faut propager les flammes à une cellule directement voisine.
Une cellule a au plus 4 cellules voisines, celles qui se trouvent au nord, à l’ouest, au sud ou à l’est. Il est donc utile de disposer d’une fonction qui, étant donné une cellule connue par sa position ligne x colonne
dans une grille carrée de côté \(\mathtt{n}\), renvoie la liste des cellules voisines. Voici une telle fonction :
voisins.py
1 2 3 4 5 6 7 | def voisins(n, i, j):
return [(a,b) for (a, b) in
[(i, j+1),(i, j-1), (i-1, j), (i+1,j)]
if a in range(n) and b in range(n)]
print(voisins(5, 2, 1))
print(voisins(5, 2, 4))
|
8 9 | [(2, 2), (2, 0), (1, 1), (3, 1)]
[(2, 3), (1, 4), (3, 4)]
|
J’ai utilisé des listes en compréhension emboîtées. Noter que cette fonction n’a pas besoin de la matrice des états states
.
On peut désormais écrire une fonction qui détermine le nouvel état de la grille en fonction de son état précédent. Et en testant, on en profitera pour mettre le feu à un arbre et observer les trois premières étapes de l’incendie :
transition.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | from random import sample
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
def voisins(n, i, j):
return [(a,b) for (a, b) in
[(i, j+1),(i, j-1), (i-1, j), (i+1,j)]
if a in range(n) and b in range(n)]
def update_states(states):
n=len(states)
to_fire=[]
for line in range(n):
for col in range(n):
if states[line][col]==2:
states[line][col]=3
for (i, j) in voisins(n, line, col):
if states[i][j]==1:
to_fire.append((i, j))
for (line,col) in to_fire:
states[line][col]=2
states = random_forest(0.6, 10)
states[0][0]=2
print('\n'.join([' '.join(map(str, L)) for L in states]))
print('------------------')
for i in range(3):
update_states(states)
print('\n'.join([' '.join(map(str, L)) for L in states]))
print('------------------')
|
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | 2 1 0 1 1 0 1 1 1 1
1 0 0 1 1 1 1 1 0 1
1 0 0 0 1 1 1 0 1 1
1 0 1 0 0 1 0 1 1 1
1 1 1 0 1 1 1 1 0 1
1 0 1 0 0 1 0 0 1 0
1 0 1 0 0 0 0 0 0 1
1 1 0 0 0 1 0 1 1 1
1 0 1 1 1 1 0 1 1 0
0 0 1 1 0 1 0 1 1 0
------------------
3 2 0 1 1 0 1 1 1 1
2 0 0 1 1 1 1 1 0 1
1 0 0 0 1 1 1 0 1 1
1 0 1 0 0 1 0 1 1 1
1 1 1 0 1 1 1 1 0 1
1 0 1 0 0 1 0 0 1 0
1 0 1 0 0 0 0 0 0 1
1 1 0 0 0 1 0 1 1 1
1 0 1 1 1 1 0 1 1 0
0 0 1 1 0 1 0 1 1 0
------------------
3 3 0 1 1 0 1 1 1 1
3 0 0 1 1 1 1 1 0 1
2 0 0 0 1 1 1 0 1 1
1 0 1 0 0 1 0 1 1 1
1 1 1 0 1 1 1 1 0 1
1 0 1 0 0 1 0 0 1 0
1 0 1 0 0 0 0 0 0 1
1 1 0 0 0 1 0 1 1 1
1 0 1 1 1 1 0 1 1 0
0 0 1 1 0 1 0 1 1 0
------------------
3 3 0 1 1 0 1 1 1 1
3 0 0 1 1 1 1 1 0 1
3 0 0 0 1 1 1 0 1 1
2 0 1 0 0 1 0 1 1 1
1 1 1 0 1 1 1 1 0 1
1 0 1 0 0 1 0 0 1 0
1 0 1 0 0 0 0 0 0 1
1 1 0 0 0 1 0 1 1 1
1 0 1 1 1 1 0 1 1 0
0 0 1 1 0 1 0 1 1 0
------------------
|
- Lignes 30-31 : on crée une forêt 10 x 10 (ligne 30) et on met le feu dans le coin en haut à gauche (ligne 31).
- Lignes 50-82 : on laisse l’incendie se propager sur trois étapes. A chaque étape, on visualise l’état de la forêt. On voit que des cendres (code 3, lignes 50 ou 61) et du feu (code 2, ligne 63 par exemple) apparaissent.
- Lignes 20-26 : on parcourt la grille (ligne 20-21) : une cellule en feu peut être changée en cendres (lignes 22-23). Toutefois, si la cellule courante est en feu et qu’une celllule voisine est un arbre, il ne faut pas mettre à jour tout de suite cette cellule (en la mettant en feu) sinon on risque de modifier le voisinage de certaines cellules non encores examinées. On place donc dans une liste temporaire
to_fire
(ligne 19) et on met à jour la grille une fois qu’elle a été parcourue (ligne 27-28).
Partie graphique¶
Il faut maintenant écrire une fonction fill(states)
qui à partir de la grille des états 0, 1, 2 ou 3, dessine dans un canevas Tkinter la forêt correspondante.
Chaque cellule est un carré de côté, disons unit=10
pixels et de couleur appropriée que l’on va choisir dans la liste suivante :
COLORS=["ivory", "lime green", "red", "gray75"]
J’ai utilisé ce nuancier pour choisir les couleurs.
Noter que l’indice dans cette liste de la couleur d’une cellule est justement le code (0, 1, 2 ou 3) qui est placé pour cette cellule dans le tableau states
. Par exemple, le code 2 correpond au feu et justement COLORS[2]="red"
.
D’autre part, un carré sera dessiné avec la méthode create_rectangle
du widget Canvas
. Ecrivons donc une fonction fill_cell
qui dessine juste la cellule en position line x col
:
def fill_cell(states, line, col):
A=(unit*col, unit*line)
B=(unit*(col+1), unit*(line+1))
state=states[line][col]
color=COLORS[state]
cnv.create_rectangle(A, B, fill=color, outline='')
Un point essentiel est que la fonction lit le plateau abstrait des états (states
) pour savoir comment dessiner (lignes 4-6). Pour alléger, je n’ai pas placé le canevas cnv
ni l’unité unit
en paramètres de la fonction car ils ne changeront pas tout le long du programme.
On en déduit une fonction qui dessine toute la forêt définie par le tableau states
:
def fill(states):
n=len(states)
for line in range(n):
for col in range(n):
fill_cell(states, line, col)
Il suffit de parcourir la grille et d’appeler la fonction fill_cell
de dessin cellule par cellule.
Voici un code complet qui affiche une forêt sans flamme :
foret_statique.py
from random import sample
from tkinter import Tk, Canvas
COLORS=["ivory", "lime green", "red", "gray75"]
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
def fill_cell(states, line, col):
A=(unit*col, unit*line)
B=(unit*(col+1), unit*(line+1))
state=states[line][col]
color=COLORS[state]
cnv.create_rectangle(A, B, fill=color, outline='')
def fill(states):
n=len(states)
for line in range(n):
for col in range(n):
fill_cell(states, line, col)
p=0.60
n=50
unit=10
# Fenêtre et canevas
root = Tk()
cnv = Canvas(root, width=unit*n, height=unit*n, background="ivory")
cnv.pack()
# Forêt aléatoire
states=random_forest(p, n)
# Plateau dessiné
fill(states)
root.mainloop()
qui produit
L’animation¶
Pour faire une animation de l’incendie, il faut :
- mettre le feu quelque part,
- faire tourner la fonction
update_states
mettant à jour la grille, - afficher sur le canevas l’évolution de la grille.
Pour mettre le feu, on se contente d’allumer une cellule au milieu du bord gauche de la forêt :
i=n//2
j=0
states[i][j]=2
comme ça on verra l’indendie progresser de la gauche vers la droite.
Passons à la fonction propagate
qui va gérer l’animation dans Tkinter :
1 2 3 4 5 | def propagate():
update_states(states)
cnv.delete("all")
fill(cnv, states, unit)
cnv.after(150, propagate)
|
Cett fonction est appelée automatiquement toutes les 150 millisecondes (ligne 5). Elle met à jour la grille des états (ligne 2) et elle redessine la forêt (ligne 4) ; pour cela, elle retire tous les items du canevas (ce qu’on appelle un « rafraîchissement », ligne 3) et elle dessine la forêt dans son nouvel état. D’où le code complet suivant :
anim.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | from random import sample
from tkinter import Tk, Canvas
COLORS=["ivory", "lime green", "red", "gray75"]
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
def voisins(n, i, j):
return [(a,b) for (a, b) in
[(i, j+1),(i, j-1), (i-1, j), (i+1,j)]
if a in range(n) and b in range(n)]
def update_states(states):
n=len(states)
to_fire=[]
for line in range(n):
for col in range(n):
if states[line][col]==2:
states[line][col]=3
for (i, j) in voisins(n, line, col):
if states[i][j]==1:
to_fire.append((i, j))
for (line,col) in to_fire:
states[line][col]=2
def fill_cell(states, line, col):
A=(unit*col, unit*line)
B=(unit*(col+1), unit*(line+1))
state=states[line][col]
color=COLORS[state]
cnv.create_rectangle(A, B, fill=color, outline='')
def fill(states):
n=len(states)
for line in range(n):
for col in range(n):
fill_cell(states, line, col)
def propagate():
update_states(states)
cnv.delete("all")
fill(states)
cnv.after(150, propagate)
p=0.62
n=50
unit=10
# Fenêtre et canevas
root = Tk()
cnv = Canvas(root, width=unit*n, height=unit*n, background="ivory")
cnv.pack()
# Forêt aléatoire
states=random_forest(p, n)
i=n//2
j=0
states[i][j]=2
# Plateau dessiné
fill(states)
propagate()
root.mainloop()
|
Pour que l’animation se lance, il faut lancer directement (ligne 70) la fonction propagate
; ensuite, cette fonction est relancée automatiquement (cf. ligne 50).
D’où l’animation annoncée.
Dans la suite de ce document, l’interface graphique sera rendue plus riche et plus interactive en permettant de
- monitorer l’évolution de l’incendie,
- lancer une nouvelle animation avec un bouton,
- cliquer directement sur une cellule pour incendier,
- modifier la densité de la forêt avec un curseur.
Automate cellulaire, percolation¶
L’activité proposée est directement inspirée de cette page du Cormas.
Simuler un incendie revient à créer ce qu’on appelle un automate cellulaire :
- automate car le système évolue par transitions successives à partir d’un état initial,
- cellulaire : les états sont enregistrés dans des cellules.
La propagation du feu va dépendre essentiellement de la densité p
des arbres dans la parcelle. Les arbres sont supposés répartis aléatoirement selon une loi uniforme. Il est concevable que les dégâts sont plus étendus si la densité est élevée. En fait, en jouant avec la densité, on se rend compte qu’elle admet une valeur critique, dite seuil de percolation :
- en deçà, l’incendie reste très limité,
- au-delà, et presque immédiatement, la propagation est beaucoup plus importante.
Une simulation pourrait produire la courbe suivante :
en abscisses, on lit la densité de la forêt, en ordonnées le pourcentage d’arbres qui ont été enflammés. Cette courbe sera construite dans la dernière partie de l’activité.
Le chercheur Mitchel Resnick a établi (1994) que la densité critique est d’environ 59 %
. Plus précisément, d’après les simulations de l’auteur, ce seuil discrimine les incendies qui partant d’un bord se propagent jusqu’au bord opposé.
Monitorer les dégâts de l’incendie¶
On souhaite connaître à chaque instant le pourcentage de forêt brûlé :
On va reprendre le code précédent. Le nombre initial d’arbres est connu, il vaut \(\mathtt{n^2\times p}\) où \(\mathtt{n}\) est la longueur du côté du carré et \(\mathtt{p}\) est la densité.
Ensuite, pour connaître le nombre total d’arbres incendiés, il suffit de tenir à jour un compteur (disons cpt
) incrémenté par le nombre d’arbres en feux sur le plateau à chaque fin de transition (disons nfires
). Ce nombre pourrait s’obtenir en modifiant la fonction de mise-à-jour de states
mais on ne va chercher à optimiser et on va se contenter de reparcourir la matrice des états states
:
nfires=sum(states[i][j]==2 for i in range(n) for j in range(n))
On peut alors calculer le pourcentage de forêt brûlée et il ne reste plus qu’à l’afficher avec un label. Cet affichage étant en temps réel, il doit être déterminé dans la fonction d’animation propagate
.
D’où le code suivant :
pourcent.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | from random import sample
from tkinter import Tk, Canvas, Label
COLORS=["ivory", "lime green", "red", "gray75"]
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
def voisins(n, i, j):
return [(a,b) for (a, b) in
[(i, j+1),(i, j-1), (i-1, j), (i+1,j)]
if a in range(n) and b in range(n)]
def update_states(states):
n=len(states)
to_fire=[]
for line in range(n):
for col in range(n):
if states[line][col]==2:
states[line][col]=3
for (i, j) in voisins(n, line, col):
if states[i][j]==1:
to_fire.append((i, j))
for (line,col) in to_fire:
states[line][col]=2
def fill_cell(states, line, col):
A=(unit*col, unit*line)
B=(unit*(col+1), unit*(line+1))
state=states[line][col]
color=COLORS[state]
cnv.create_rectangle(A, B, fill=color, outline='')
def fill(states):
n=len(states)
for line in range(n):
for col in range(n):
fill_cell(states, line, col)
def propagate():
global cnt
update_states(states)
nfires=sum(states[i][j]==2 for i in range(n) for j in range(n))
cnt+=nfires
percent = int(cnt/ntrees*100)
cnv.delete("all")
fill(states)
lbl["text"]="%3s %%" %percent
cnv.after(150, propagate)
p=0.62
n=70
unit=8
ntrees=int(n*n*p)
# Fenêtre et canevas
root = Tk()
cnv = Canvas(root, width=unit*n, height=unit*n, background="ivory")
cnv.pack(side="left")
lbl=Label(root,text=" 0 %", font='Arial 25 bold', bg='pink', width=5)
lbl.pack(padx=10, pady=10)
# Forêt aléatoire
states=random_forest(p, n)
i=n//2
j=0
states[i][j]=2
cnt=1
# Plateau dessiné
fill(states)
propagate()
root.mainloop()
|
Les principales modifications par rapport au code de anim.py
sont les suivantes :
- on initialise le compteur
cnt
à 1 (ligne 77) et non pas à 0 puisqu’il y a un foyer initial (ligne 76); - on calcule le nombre total
ntrees
(ligne 61) d’arbres ; - on créé un label pour afficher le pourcentage (lignes 68-69) ; la couleur de fond du label est rose (pink).
- après chaque transition (ligne 49), on calcule le nombre d’arbres encore en feu (ligne 50) et on incrémente le compteur (ligne 51) ce qui permet de calculer le pourcentage (lignes 52) ;
- on met à jour le label (ligne 55); j’ai utilisé la syntaxe classique des chaînes formatées. En outre, avec la syntaxe classique, le signe
%
est traduit par les deux caractères%%
.
Il resterait à améliorer un point : quand l’incendie est éteint, l’animation continue à tourner ; pour s’en convaincre, il suffit d’écrire une instruction du genre
print(randrange(42))
(après avoir importé randrange
) à la fin de la fonction propagate
(après la ligne 56) et de constater que, malgré la fin de l’incendie, des valeurs continuent à s’afficher dans la console, signe que l’animation tourne :
Il est assez simple de remédier à ce problème ; en effet, s’il n’y a plus aucun nouveau foyer, l’incendie est terminé (il n’y a pas de reprise de feu). Il suffit donc, lorsque nfires
vaut 0 (ligne 50), de faire un return
dans la fonction propagate
, juste avant la relance de la fonction propagate
(avant ligne 56) :
lbl["text"]="%3s %%" %percent
if nfires == 0:
return
cnv.after(150, propagate)
D’où le code complet :
anim_complet.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | from random import sample
from tkinter import Tk, Canvas, Label
from random import randrange
COLORS=["ivory", "lime green", "red", "gray75"]
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
def voisins(n, i, j):
return [(a,b) for (a, b) in
[(i, j+1),(i, j-1), (i-1, j), (i+1,j)]
if a in range(n) and b in range(n)]
def update_states(states):
n=len(states)
to_fire=[]
for line in range(n):
for col in range(n):
if states[line][col]==2:
states[line][col]=3
for (i, j) in voisins(n, line, col):
if states[i][j]==1:
to_fire.append((i, j))
for (line,col) in to_fire:
states[line][col]=2
def fill_cell(states, line, col):
A=(unit*col, unit*line)
B=(unit*(col+1), unit*(line+1))
state=states[line][col]
color=COLORS[state]
cnv.create_rectangle(A, B, fill=color, outline='')
def fill(states):
n=len(states)
for line in range(n):
for col in range(n):
fill_cell(states, line, col)
def propagate():
global cnt
update_states(states)
nfires=sum(states[i][j]==2 for i in range(n) for j in range(n))
cnt+=nfires
percent = int(cnt/ntrees*100)
cnv.delete("all")
fill(states)
lbl["text"]="%3s %%" %percent
if nfires == 0:
return
cnv.after(150, propagate)
print(randrange(42))
p=0.62
n=70
unit=8
ntrees=int(n*n*p)
# Fenêtre et canevas
root = Tk()
cnv = Canvas(root, width=unit*n, height=unit*n, background="ivory")
cnv.pack(side="left")
lbl=Label(root,text=" 0 %", font='Arial 25 bold', bg='pink', width=5)
lbl.pack(padx=10, pady=10)
# Forêt aléatoire
states=random_forest(p, n)
i=n//2
j=0
states[i][j]=2
cnt=1
# Plateau dessiné
fill(states)
propagate()
root.mainloop()
|
Note que l’instruction print(randrange(42))
à la fin de la fonction propagate
permettent de surveiller que la fin de l’animation est effective.
Mettre le feu avec la souris¶
Plutôt que d’imposer un départ du feu au milieu du bord gauche du plateau, on souhaite pouvoir choisir l’emplacement d’où va démarrer le feu. Le plus simple est de cliquer sur un arbre pour provoquer l’incendie :
On va donc devoir associer (bind) le clic de souris sur le canevas à une fonction de rappel qu’on appelera fire
, ce qui se fera comme ceci :
cnv.bind("<Button>", fire)
ce qui permettra de calculer l’indice de ligne et de colonne de la cellule qui a été cliquée. On va reprendre le code de anim.py
. Il suffit donc de remplacer i
et j
dans le code ci-dessous
1 2 3 | i=n//2
j=0
states[i][j]=2
|
pour que (i, j)
soit la position du clic de souris (s’il est sur un arbre).
Par ailleurs dans le code anim.py
. (lignes 68-72) on avait :
# Plateau dessiné
fill(states)
propagate()
root.mainloop()
ce qui doit désormais être supprimé car l’animation doit être lancée par le clic de souris et donc dans la fonction fire
. D’où le code suivant :
feu_clic.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | from random import sample
from tkinter import Tk, Canvas
COLORS=["ivory", "lime green", "red", "gray75"]
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
def voisins(n, i, j):
return [(a,b) for (a, b) in
[(i, j+1),(i, j-1), (i-1, j), (i+1,j)]
if a in range(n) and b in range(n)]
def update_states(states):
n=len(states)
to_fire=[]
for line in range(n):
for col in range(n):
if states[line][col]==2:
states[line][col]=3
for (i, j) in voisins(n, line, col):
if states[i][j]==1:
to_fire.append((i, j))
for (line,col) in to_fire:
states[line][col]=2
def fill_cell(states, line, col):
A=(unit*col, unit*line)
B=(unit*(col+1), unit*(line+1))
state=states[line][col]
color=COLORS[state]
cnv.create_rectangle(A, B, fill=color, outline='')
def fill(states):
n=len(states)
for line in range(n):
for col in range(n):
fill_cell(states, line, col)
def propagate():
update_states(states)
cnv.delete("all")
fill(states)
cnv.after(400, propagate)
def fire(event):
i, j=event.y//unit, event.x//unit
if states[i][j]==1:
states[i][j]=2
fill_cell(states, i, j)
fill(states)
propagate()
p=0.62
n=50
unit=10
# Fenêtre et canevas
root = Tk()
cnv = Canvas(root, width=unit*n, height=unit*n, background="ivory")
cnv.pack()
cnv.bind("<Button>", fire)
# Forêt aléatoire
states=random_forest(p, n)
# Plateau dessiné
fill(states)
root.mainloop()
|
- Lignes 53-59 : la fonction de rappel du clic de souris sur le canevas.
- Ligne 70 : liaison du clic sur le canevas et de la fonction
fire
. - Ligne 54 : on récupère la position du clic, on en déduit les indices de ligne et de colonne de la cellule cliquée ; on met à jour
states
(ligne 57) et on lance la fonction d’animation (ligne 59) si on a bien cliqué sur un arbre (ligne 55).
Le code ci-dessus a néanmoins un problème si on reclique sur un arbre : cela va lancer deux animations concurrentes du même plateau ce qui va fausser l’animation. Pour éviter ce problème mais accepter malgré tout la possibilité que plusieurs foyers puissent être créées par l’utilisateur, il suffit de créer un drapeau (ci-dessous running
, ligne 2 et 9) qui surveille l’exécution de l’animation :
1 2 3 4 5 6 7 8 9 10 | def fire(event):
global running
i, j=event.y//unit, event.x//unit
if states[i][j]==1:
states[i][j]=2
fill_cell(states, i, j)
fill(states)
if not running:
running=True
propagate()
|
Si on reclique sur le plateau et qu’une animation est en cours, il suffit de ne pas relancer l’animation (ligne 8) mais d’accepter la mise à jour de la matrice des états (ligne 5).
Dernier détail : on observera que si on clique à la frontière du canevas en bas et à droite qu’on obtient un débordement d’indice : on a cliqué sur le bord d’une cellule et l’indice est un de plus que l’indice autorisé. Pour régler ce problème, il suffit de rogner légèrement sur la taille du canevas, et lui retirer 2 pixels (dernière ligne ci-dessous):
n=50
unit=10
# Fenêtre et canevas
root = Tk()
cnv = Canvas(root, width=unit*n-2, height=unit*n-2, background="ivory")
Le code complet est le suivant :
feu_clic_var.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | from random import sample
from tkinter import Tk, Canvas
COLORS=["ivory", "lime green", "red", "gray75"]
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
def voisins(n, i, j):
return [(a,b) for (a, b) in
[(i, j+1),(i, j-1), (i-1, j), (i+1,j)]
if a in range(n) and b in range(n)]
def update_states(states):
n=len(states)
to_fire=[]
for line in range(n):
for col in range(n):
if states[line][col]==2:
states[line][col]=3
for (i, j) in voisins(n, line, col):
if states[i][j]==1:
to_fire.append((i, j))
for (line,col) in to_fire:
states[line][col]=2
def fill_cell(states, line, col):
A=(unit*col, unit*line)
B=(unit*(col+1), unit*(line+1))
state=states[line][col]
color=COLORS[state]
cnv.create_rectangle(A, B, fill=color, outline='')
def fill(states):
n=len(states)
for line in range(n):
for col in range(n):
fill_cell(states, line, col)
def propagate():
update_states(states)
cnv.delete("all")
fill(states)
cnv.after(400, propagate)
def fire(event):
global running
i, j=event.y//unit, event.x//unit
if states[i][j]==1:
states[i][j]=2
fill_cell(states, i, j)
fill(states)
if not running:
running=True
propagate()
p=0.62
n=50
unit=10
running=False
# Fenêtre et canevas
root = Tk()
cnv = Canvas(root, width=unit*n, height=unit*n, background="ivory")
cnv.pack()
cnv.bind("<Button>", fire)
# Forêt aléatoire
states=random_forest(p, n)
# Plateau dessiné
fill(states)
root.mainloop()
|
Curseur pour modifier la densité des arbres¶
On souhaite ajouter un peu d’interactivité en permettant à l’utilisateur de modifier la densité de la forêt, ce qui lui permet de mieux se rendre compte du seuil de percolation. La modification se fera à l’aide d’un curseur (le widget Scale
) pour obtenir le résultat suivant :
On va repartir du code foret_statique.py
qui affiche une vue statique de la forêt :
foret_statique.py
from random import sample
from tkinter import Tk, Canvas
COLORS=["ivory", "lime green", "red", "gray75"]
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
def fill_cell(states, line, col):
A=(unit*col, unit*line)
B=(unit*(col+1), unit*(line+1))
state=states[line][col]
color=COLORS[state]
cnv.create_rectangle(A, B, fill=color, outline='')
def fill(states):
n=len(states)
for line in range(n):
for col in range(n):
fill_cell(states, line, col)
p=0.60
n=50
unit=10
# Fenêtre et canevas
root = Tk()
cnv = Canvas(root, width=unit*n, height=unit*n, background="ivory")
cnv.pack()
# Forêt aléatoire
states=random_forest(p, n)
# Plateau dessiné
fill(states)
root.mainloop()
Le curseur va lire un pourcentage (la densité de la forêt), ce qui correspond à p
dans le code ci-dessus. Ce pourcentage va être traité par une fonction de commande que l’on appelera make_forest(percent)
. Cette fonction devra se charger de construire un plateau states
avec la bonne densité et faire comme ci-dessus pour dessiner la forêt sur le canevas.
Créons un curseur vertical :
curseur = Scale(root, orient = "vertical", command=make_forest, from_=100,
to=0, length=200)
curseur.set(100*p)
curseur.pack(side='right')
Le curseur sera initialisé à une valeur p
par défaut placée dans le code (ci-dessous, p=0.5
). Lorsqu’on clique sur le curseur, il appelle une fonction make_forest
. Voici le code :
curseur.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | from random import sample, randrange
from tkinter import Tk, Canvas, Scale
COLORS=["ivory", "lime green", "red", "gray75"]
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
def fill(states):
n=len(states)
for line in range(n):
for col in range(n):
A=(unit*col, unit*line)
B=(unit*(col+1), unit*(line+1))
state=states[line][col]
color=COLORS[state]
cnv.create_rectangle(A, B, fill=color, outline='')
def init(p):
global states
states=random_forest(p, n)
fill(states)
def make_forest(percent):
cnv.delete("all")
p=float(percent)/100
init(p)
p=0.50
n=80
unit=8
# Fenêtre et canevas
root = Tk()
cnv = Canvas(root, width=unit*n, height=unit*n, background="ivory")
cnv.pack(side="left")
trees=init(p)
fill(states)
curseur = Scale(root, orient = "vertical", command=make_forest, from_=100,
to=0, length=200)
curseur.set(100*p)
curseur.pack(side='right')
init(p)
root.mainloop()
|
- On crée une fonction
init(p)
(ligne 25) qui, connaissant la densitép
, crée la matrice des états initiaux (ligne 27) et dessine la forêt correspondante (ligne 28). La fonctioninit
sert à montrer un premier plateau (ligne 44) et à recréer un plateau si le curseur est déplacé (ligne 33). - La fonction
make_forest
lit sur le curseur la valeurpercent
indiquée par l’utilisateur. Cette valeur qui est une chaîne de caractères est convertie en un flottant représentant la densité de la forêt (ligne 32). Le plateau est effacé (ligne 31) pour permettre la création du nouveau plateau qui est généré par un appel à la fonctioninit
(ligne 33).
Variante¶
Dans le code précédent, lorque le curseur est modifié, la forêt est complètement renouvelée. On pourrait imaginer que la forêt est uniformément
- complétée si la densité demandée est plus forte que la densité de la forêt visible,
- allégée si la densité demandée est plus faible
ce qui donne un comportement plus lisse :
Il faut donc écrire une fonction set_density
qui va modifier l’état du plateau pour l’ajuster à la nouvelle densité. Voici un code possible :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def set_density(states, p):
n=len(states)
trees= [(i,j) for i in range(n) for j in range(n) if states[i][j]==1]
nontrees=[(i,j) for i in range(n) for j in range(n) if states[i][j]!=1]
density=len(trees)/n**2
new_trees=round(n*n*p)
before=len(trees)
now=len(nontrees)
delta=abs(new_trees-before)
if new_trees>=before:
for (i, j) in sample(nontrees, delta):
states[i][j]=1
else:
for (i, j) in sample(trees, delta):
states[i][j]=0
|
On recherche
- la densité actuelle de la forêt (lignes 5)
- on localise les cellules contenant des arbres (ligne 3) et on calcule le nombre d’arbres (ligne 7)
- on localise les cellules où il y a le sol (ligne 4)
- le nombre d’arbres de la future forêt (ligne 6)
Selon les besoins, on supprime aléatoirement des arbres parmi les arbres existants ou on rajoute des arbres sur les emplacements libres.
D’où le code suivant :
curseur_progressif.py
from random import sample, randrange
from tkinter import Tk, Canvas, Scale
COLORS=["ivory", "lime green", "red", "gray75"]
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
def fill(states):
n=len(states)
for line in range(n):
for col in range(n):
A=(unit*col, unit*line)
B=(unit*(col+1), unit*(line+1))
state=states[line][col]
color=COLORS[state]
cnv.create_rectangle(A, B, fill=color, outline='')
def init(p):
global states
states=random_forest(p, n)
fill(states)
def set_density(states, p):
n=len(states)
trees= [(i,j) for i in range(n) for j in range(n) if states[i][j]==1]
nontrees=[(i,j) for i in range(n) for j in range(n) if states[i][j]!=1]
density=len(trees)/n**2
new_trees=round(n*n*p)
before=len(trees)
now=len(nontrees)
delta=abs(new_trees-before)
if new_trees>=before:
for (i, j) in sample(nontrees, delta):
states[i][j]=1
else:
for (i, j) in sample(trees, delta):
states[i][j]=0
def make_forest(percent):
cnv.delete("all")
p=float(percent)/100
set_density(states,p)
fill(states)
p=0.5
n=100
unit=8
# Fenêtre et canevas
root = Tk()
cnv = Canvas(root, width=unit*n, height=unit*n, background="ivory")
cnv.pack(side="left")
trees=init(p)
fill(states)
curseur = Scale(root, orient = "vertical", command=make_forest, from_=100,
to=0, length=200)
curseur.set(100*p)
curseur.pack(side='right')
root.mainloop()
Application complète¶
On veut désormais que l’animation possède toutes les fonctionnalités précédentes réunies :
- un curseur pour modifier la densité de la forêt
- enflammer par clics de souris
- le label de pourcentage
- un bouton pour relancer une animation.
La difficulté est que l’animation peut être initialisée plusieurs fois et donc qu’il faut écrire une fonction d’initilisation définissant certains paramètres de l’animation qui doivent être visibles d’autres fonctions.
Voici un code réalisant les fonctionnalités cités. Seules les fonctions véritablement nouvelles sont montrées :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | # Code omis
def init():
global states, cpt, ntrees, running
p=int(curseur.get())/100
running=False
cpt=0
lbl["text"]="%3s %%" %0
curseur["state"]='normal'
states=random_forest(p, n)
ntrees=int(n*n*p)
cnv.delete(ALL)
fill(states)
def set_density(states, p):
n=len(states)
trees= [(i,j) for i in range(n) for j in range(n) if states[i][j]==1]
nontrees=[(i,j) for i in range(n) for j in range(n) if states[i][j]!=1]
density=len(trees)/n**2
new_trees=int(n*n*p)
before=len(trees)
now=len(nontrees)
delta=abs(new_trees-before)
if new_trees>=before:
for (i, j) in sample(nontrees, delta):
states[i][j]=1
else:
for (i, j) in sample(trees, delta):
states[i][j]=0
def make_forest(percent):
global ntrees
cnv.delete("all")
p=float(percent)/100
ntrees=int(n*n*p)
set_density(states,p)
fill(states)
def propagate():
global cpt, running
update_states(states)
nfires=sum(states[i][j]==2 for i in range(n) for j in range(n))
cpt+=nfires
percent = int(cpt/ntrees*100)
cnv.delete("all")
fill(states)
lbl["text"]="%3s %%" %percent
if nfires==0:
running=False
return
cnv.after(150, propagate)
def fire(event):
global running, cpt
i, j=event.y//unit, event.x//unit
if states[i][j]==1:
states[i][j]=2
fill_cell(i, j)
cpt+=1
if not running:
running=True
curseur["state"]='disabled'
propagate()
# Code omis
init()
root.mainloop()
|
Examinons comment s’effectue l’exécution de l’application.
A tout instant, on peut relancer une nouvelle animation en cliquant sur le bouton New
. On peut créer à la souris de nouveaux foyers pendant un incendie qui est déjà en train de se propager. Avant de déclencher l’incendie, on peut modifier la densité. Une fois l’incendie en cours, le curseur est désactivé (ligne 70). Pour le réactiver, il faut créer une nouvelle forêt, cf. ligne 10.
La fonction init
¶
- La fonction
init
est appelée (ligne 73). Comme son nom l’indique, elle initialise le jeu et affiche une forêt statique. - Certaines variables définies dans
init
sont indispensables à d’autres fonctions, par exemplestates
doit être connue de pratiquement toutes les fonctions dont on ne maîtrise pas le passage des arguments, telles que la fonctionfire
. Donc, avec ce découpage, on est obligé d’utiliser une variable globale pourstates
. - On introduit une nouvelle variable
running
qui est un drapeau indiquant si l’animation tourne ou pas. Au départ, elle est àFalse
. Cela permet de désactiver l’animation ou d’empêcher que deux animations aient lieu en même temps, cf. lignes 66-69. - Une fois la fonction
init
exécutée, l’interface est en attente d’une activité de la souris (clic sur le canevas, sur le bouton ou déplacement du curseur). - La densité ne doit pas être gravée dans la fonction
init
mais être lue depuis le curseur lui même (ligne 6) puisque si on réinitialise le canevas, il est attendu que la densité de la forêt corresponde à celle indiquée par le curseur. - Comme la fonction
init
peut être appelée après une animation, il faut remettre à zéro le pourcentage de dégâts (ligne 8).
La fonction make_forest
¶
Elle est appelée (ligne 32) suite à un mouvement du curseur. Comme la densité change, il faut mettre à jour le nombre d’arbres ntrees
(ligne 37) dont on a besoin pour calculer le pourcentage. La variable ntrees
doit être globale pour être connue par exemple de la fonction propagate
(ligne 48).
Les fonctions propagate
et fire
¶
Elles ont assez peu changé.
- S’il n’y a aucun arbre enflammé (ligne 52), l’incendie s’arête et donc l’animation se termine, cf . ligne 54. Le drapeau
running
doit être mis àFalse
(ligne 53). Cela oblige à placerrunning
englobal
(ligne 43). - Lorsqu’on clique sur une cellule pour l’enflammer, il faut penser à incrémenter le compteur, cf. ligne 65 (sinon, le calcul de pourcentage est faussé).
Voci le code complet :
feu_foret_final.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | from random import sample, randrange
from tkinter import Tk, Canvas, Scale, Button, Label, N, ALL
COLORS=["ivory", "lime green", "red", "gray75"]
def random_forest(p, n):
units=[(line,col) for col in range(n) for line in range(n)]
ntrees=int(n**2*p)
trees=sample(units,ntrees)
states=[[0]*n for _ in range(n)]
for (i,j) in trees:
states[i][j]=1
return states
def voisins(n, i, j):
return [(a,b) for (a, b) in
[(i, j+1),(i, j-1), (i-1, j), (i+1,j)]
if a in range(n) and b in range(n)]
def fill_cell(states, line, col):
A=(unit*col, unit*line)
B=(unit*(col+1), unit*(line+1))
state=states[line][col]
color=COLORS[state]
cnv.create_rectangle(A, B, fill=color, outline='')
def fill(states):
n=len(states)
for line in range(n):
for col in range(n):
fill_cell(states, line, col)
def update_states(states):
n=len(states)
to_fire=[]
for line in range(n):
for col in range(n):
if states[line][col]==2:
states[line][col]=3
for (i, j) in voisins(n, line, col):
if states[i][j]==1:
to_fire.append((i, j))
for (line,col) in to_fire:
states[line][col]=2
def init():
global states, cpt, ntrees, running
p=int(curseur.get())/100
running=False
cpt=0
lbl["text"]="%3s %%" %0
curseur["state"]='normal'
states=random_forest(p, n)
ntrees=int(n*n*p)
cnv.delete(ALL)
fill(states)
def set_density(states, p):
n=len(states)
trees= [(i,j) for i in range(n) for j in range(n) if states[i][j]==1]
nontrees=[(i,j) for i in range(n) for j in range(n) if states[i][j]!=1]
density=len(trees)/n**2
new_trees=int(n*n*p)
before=len(trees)
now=len(nontrees)
delta=abs(new_trees-before)
if new_trees>=before:
for (i, j) in sample(nontrees, delta):
states[i][j]=1
else:
for (i, j) in sample(trees, delta):
states[i][j]=0
def make_forest(percent):
global ntrees
cnv.delete("all")
p=float(percent)/100
ntrees=int(n*n*p)
set_density(states,p)
fill(states)
def propagate():
global cpt, running
update_states(states)
nfires=sum(states[i][j]==2 for i in range(n) for j in range(n))
cpt+=nfires
percent = int(cpt/ntrees*100)
cnv.delete("all")
fill(states)
lbl["text"]="%3s %%" %percent
if nfires==0:
running=False
return
cnv.after(150, propagate)
def fire(event):
global running, cpt
i, j=event.y//unit, event.x//unit
if states[i][j]==1:
states[i][j]=2
fill_cell(states, i, j)
cpt+=1
if not running:
running=True
curseur["state"]='disabled'
propagate()
n=50
unit=10
# Fenêtre et canevas
root = Tk()
cnv = Canvas(root, width=unit*n-2, height=unit*n-2, background="ivory")
cnv.grid(row=0, column=0, rowspan=4)
btn=Button(root,text="New", font='Arial 15 bold', command=init, width=8)
btn.grid(row=0, column=1, sticky=N)
lbl=Label(root,text="%3s %%" %0, font='Arial 15 bold', bg='pink', width=5)
lbl.grid(row=2, column=1, sticky=N)
# Clic qui met le feu
cnv.bind("<Button-1>", fire)
curseur = Scale(root, orient = "vertical", command=make_forest, from_=100,
to=0, length=200)
curseur.set(50)
curseur.grid(row=3, column=1)
init()
root.mainloop()
|
Simulations numériques¶
Observons la propagation de l’incendie sur l’animation :
et en même temps, reprenons le code de la fonction de transition d’un état de la forêt à l’état suivant :
1 2 3 4 5 6 7 8 9 10 11 12 | def update_states(states):
n=len(states)
to_fire=[]
for line in range(n):
for col in range(n):
if states[line][col]==2:
states[line][col]=3
for (i, j) in voisins(n, line, col):
if states[i][j]==1:
to_fire.append((i, j))
for (line,col) in to_fire:
states[line][col]=2
|
La double boucle for
(lignes 4-5) parcourt toute la grille à la recherche de cellules en feu (ligne 6). Or, comme le montre l’animation, le front de flammes représente une partie très réduite de la zone parcourue et donc, il est bien plus efficace de mémoriser les positions des cellules en flammes puis de les passer en revue car c’est seulement d’elles que proviennent les nouveaux foyers.
Pour pouvoir effectuer quelques simulations numériques, on va donc coder autrement la fonction de mise à jour update_states
en sorte qu’elle renvoie la liste des cellules en feu à l’issue de la transition. Voici un code possible :
1 2 3 4 5 6 7 8 9 10 11 12 13 | def update_states(states, fires):
n=len(states)
to_fire=[]
for (line, col) in fires:
for (i, j) in voisins(n, line, col):
if states[i][j]==1:
to_fire.append((i, j))
for (line, col) in to_fire:
states[line][col]=2
for (line, col) in fires:
states[line][col]=3
return list(set(to_fire))
|
- Ligne 1 : la fonction a besoin des positions des cellules en feu, cf. le paramètre
fires
. En effet, cf. ligne 4, la fonction parcourt la liste des cellules en feu. - Lignes 5-7 : si une cellule est en feu (ligne 4), on regarde si parmi ses voisines (ligne 5) se trouverait un arbre (ligne 6), auquel cas cet arbre va prendre feu et est donc inclus dans la liste des arbres nouvellement en feu (ligne 7).
- Sous peine de fausser l’obtention du nouvel état, on ne met pas à jour directement la matrice des états et on place dans une liste temporaire, ici
to_fire
, les nouveaux foyers ligne 7). - Les changement d’états ne peuvent provenir que des cellules initialement en flammes. Donc une fois toutes les cellules en feu examinées (à partir de la ligne 8), la transition est achevée et il est possible de mettre à jour la plateau des cellules en cendres et des cellules inflammées (lignes 8-11).
- Ligne 13 : pour les besoins de futures itérations, la fonction doit renvoyer la liste des feux qui sont apparus. On ne peut renvoyer la liste
to_fire
car elle peut contenir des doublons. C’est pour les éliminer qu’on applique le constructeurset
à la liste, suivant une technique bien connue.
Phase de test¶
Pour essayer d’approcher expérimentalement le seuil de percolation, on se donne une densité p
, ci-dessous p
variera entre 0.5 et 0.7 et on répète un incendie un nombre repet
de fois, ci-dessous repet=100
, sur une parcelle de taille \(\mathtt{n\times n}\) donnée, ci-dessous \(\mathtt{n=150}\) et on calcule la moyenne des proportions de forêt brûlée. Voici le code :
simulation.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | from random import sample, randrange
def random_forest(p, n):
units = [(line, col) for col in range(n) for line in range(n)]
ntrees = int(n**2*p)
trees = sample(units, ntrees)
states = [[0]*n for _ in range(n)]
for (i, j) in trees:
states[i][j] = 1
return states, trees
def voisins(n, i, j):
return [(a, b) for (a, b) in
[(i, j+1), (i, j-1), (i-1, j), (i+1, j)]
if a in range(n) and b in range(n)]
def start_fire(states, trees):
i, j = trees[randrange(len(trees))]
states[i][j] = 2
return (i, j)
def update_states(states, fires):
n = len(states)
to_fire = []
for (line, col) in fires:
for (i, j) in voisins(n, line, col):
if states[i][j] == 1:
to_fire.append((i, j))
for (line, col) in to_fire:
states[line][col] = 2
for (line, col) in fires:
states[line][col] = 3
return list(set(to_fire))
def test(n, p, repet):
stats = []
for _ in range(repet):
states, trees = random_forest(p, n)
i, j = start_fire(states, trees)
ntrees = len(trees)
nfire = 1
foyers = [(i, j)]
while True:
foyers = update_states(states, foyers)
fire = len(foyers)
nfire += fire
if fire == 0:
break
stats.append(nfire/ntrees)
return sum(stats)/repet
n=150
repet=100
f=lambda p:test(n,p,repet)
P = [0.5+k/1000 for k in range(0, 201, 5)]
Q = list(map(f, P))
|
- Ligne 56 : on examine l’incendie pour les densités possibles entre 0.5 et 0.7 par tranches de 5/1000.
- Le traitement d’un seul incendie (lignes 39-49).
- Pour chaque densité
p
(ligne 36), on évalue le taux de forêt consumée (par rapport à la forêt intacte, cf. ligne 50) en répétantrepet
incendies (ligne 38), chaque taux étant placé dans la listestats
(lignes 37 et 51).
L’exécution donne sur un processeur i5 :
$ time python3 simulation.py
real 1m51,388s
user 1m51,304s
sys 0m0,017s
$
ce qui est déjà assez long. La simulation serait complètement irréalisable si on avait gardé la première méthode de traitement de la propagation de l’incendie, cf. le code transition.py
. Pour accélérer les calculs, on peut utiliser l’interpréteur PyPy ce qui divise ici le temps d’exécution par plus d’un facteur 5. Il serait possible d’accélérer l’exécution en utilisant Numpy et en vectorisant le code, ou alors d’écrire le code en Cython ou encore avec Numpy + Numba ce qui donnerait sans doute de très bonnes performances vu le type de code.
Visualisation¶
Pour visualiser, il faut avoir installé les packages Numpy, SciPy et Matplotlib. Il serait plus commode de placer le code dans une feuille Jupyter Notebook. On peut observer la courbe fittée des résultats de la simulation précédente :
Voici le code correspondant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | from matplotlib.pyplot import figure
from scipy.optimize import curve_fit
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('ggplot')
from simulation import P as X, Q as Y
print("Data generated!")
def sigmoid(x, a, b, c, d):
return a/(b+np.exp(-c*x))+d
x = np.array(X)
y = np.array(Y)
popt, _ = curve_fit(sigmoid, x, y, maxfev=5000)
# Ordonnées de la courbe fittée
yy = sigmoid(x, *popt)
# Dessin
figure(num=None, figsize=(8, 6), dpi=80, facecolor='w', edgecolor='k')
plt.plot(x, yy, color="blue", linewidth=3)
plt.plot(x, y, 'o', markersize=4, color='red')
plt.xlabel('Densité')
plt.ylabel('Taux forêt brûlée')
plt.show()
|
- Ligne 7 : on a besoin du fichier
simulation.py
qui génère le nuages de points. - lignes 2-3 : pour le calcul de l’ajustement, on a besoin de SciPy qui lui même utilise Numpy.
- Lignes 11-12 : Le nuage (cf. lignes 14-15) est fitté (cf. ligne 16) par une sigmoïde qu’il faut définir (lignes 11-12) et dépendant de 4 paramètres
a
,b
,c
etd
. - Lignes 14-15 : il faut convertir les listes en tableaux Numpy.
- Ligne 16 : les 4 paramètres sont calculés dans
popt
par la fonctioncurve_fit
pour un ajustement par la méthode des moindres carrés ce qui va permettre de tracer la courbe fittée (en bleu sur le dessin). - Ligne 27 : utile si on n’exécute pas le fichier dans une feuille Jupyter Notebook.