Propagation d'un feu de forêt


INTRODUCTION

Il s'agit d'un automate cellulaire et l'algorithme original revient à Pierre Audibert, professeur d'informatique à l'Université de Paris 8. Ces merveilleux supports de cours sont accessibles uniquement sur papier au secrétariat d'informatique de l'UFR 6 :

AUDIBERT Pierre, Ordre et chaos, fractales, pavages, Université de Paris 8, département Informatique, support de cours 2000.

AUDIBERT Pierre, Algorithmes et programmation, applications en combinatoire, probabilités, théorie des nombres, géométrie, Université de Paris 8, département Informatique, support de cours 1995, 1997, 1999.

TELECHARGER

aaaTexte des explications, aaaExecutable (Windows), aaaSource (utilise la librairie Allegro)

 

EXPLICATIONS

1. Principe

L’automate cellulaire fait évoluer un plan en soumettant chaque position qui le constitue à des lois de transition. Ces lois se fondent sur le voisinage des positions entre elles, nombre de voisins, qualité des voisins etc. Là il s’agit de représenter un phénomène de propagation, le feu dans une forêt.
L’espace est celui d’une matrice. Au départ il est vide et toutes les positions sont à zéro. Des arbres y sont disposés, c’est-à-dire que des positions prennent une valeur, par exemple un. Des foyers d’incendies sont ajoutés et des positions sur la valeur un des arbres prennent une valeur autre, par exemple deux. Tout est alors prêt pour la mise en œuvre du processus qui est très simple : toutes les positions de la matrice sont parcourues, si la position est un arbre et qu’il y a une position voisine sur feu, elle passe à la valeur feu ; en revanche, si la position est à feu, et bien elle passe à cendre et prend une autre valeur disons 3. A partir de ce petit moteur plusieurs extensions sont possibles. C’est souvent le cas avec des automates cellulaires. L’automate cellulaire est une figure attachante pour l’imagination parce qu’elle soumet facilement des énigmes, par exemple ici la quête d’un feu infini.

2 Mise en forme et initialisations

Tout d’abord, la forme de l’espace est une matrice de int « P » (Plan) et sa taille est définie par deux macros. Le fonctionnement réclame une seconde matrice « NP » (Nouveau Plan) en miroir de la première afin de stocker les résultats de la transition pour l’ensemble des positions. Ce sont au début du programme les déclarations :

aaaa#define TX 200
aaaa#define TY 150
aaaaint P[TX][TY];
aaaaint NP[TX][TY];

Les différentes valeurs possibles pour chaque position sont isolées avec trois macros :

aaaa#define BOIS 1
aaaa#define FEU 2
aaaa#define CENDRE 3

Sur le plan visuel les arbres sont représentés par des carreaux de couleur sur l’écran et l’affichage est facilité par une série de macros utilitaires. Avec la taille de l’écran, il y a la définition d’une longueur « pasx » et d’une hauteur « pasy » des carreaux. Elles correspondent au rapport entre la taille de l’écran et la taille de la matrice :

aaaa#define ECRAN_X 800
aaaa#define ECRAN_Y 600
aaaa#define pasx ECRAN_X/TX
aaaa#define pasy ECRAN_Y/TY

Ensuite deux macros donnent la coïncidence sur l’écran des positions de la matrice (Compte tenu de pasx et pasy) :

aaaa#define Y(n) (n)*pasy
aaaa#define X(n) (n)*pasx

Reste encore deux macros pour la définition d’une densité des arbres lors de l’initialisation de la forêt. Toutes ces valeurs sont modifiables facilement pour toutes sortes de tests :

aaaa#define MAXI 1000
aaaa#define SEUIL 0.6*MAXI

Enfin, trois variables globales. Une palette, une image intermédiaire (Nous optons pour un double buffering) et un entier pour compter le nombre des arbres en feu.

aaaaPALETTE pal;
aaaaBITMAP *page;
aaaaint cmpt_feu;

Initialisation des bois

L’initialisation des bois consiste à parcourir la matrice P et pour chaque position attribuer soit la valeur BOIS en fonction de la valeur de SEUIL, soit la valeur 0. C’est la fonction « init_bois » :

(1)aaaavoid init_bois( void )
aaaa{
aaaaint i, j, hasard;

(2)aaaa for ( i = 0; i < TX; i++ )
aaaaaaaaaafor ( j = 0; j < TY; j++ ){

(3)aaaaaaaaaahasard = rand( )%MAXI;
aaaaaaaaaaa aif ( hasard < SEUIL )
aaaaaaaaaaa aaaaP[ i ][ j ] = BOIS;
aaaaaaaaaaaaelse
aaaaaaaaaaaaaa aP[ i ][ j ] = 0;
aaaaaaaaaa}
aaaa}

(1) « init_bois » ne prend pas d’argument et ne renvoie rien.

(2) Boucles for imbriquées qui donne accès à chaque position de la matrice.

(3) Affectation de la valeur de retour de l’appel de la fonction « rand » à la variable « hasard ». Ensuite comparaison de hasard à la valeur de SEUIL. Si hasard est inférieur alors la position ( i, j ) dans la matrice prend la valeur BOIS, sinon elle prend la valeur 0.

Initialisation du feu

En ce qui concerne le feu un front est allumé en bordure gauche de l’écran. C’est la fonction « init_feu » :

(1)aaaaint init_feu(void)
aaaa{
aaaaint i, j, cmpt;

(2)aaaaafor ( i = 1, j = 0, cmpt = 0; j < TY; j++ )
aaaa aaaaaaif ( P[ i ][ j ] == BOIS ){
aaaaaaaa aaaaaP[ i ][ j ] = FEU;
aaaaaaaaaaaa acmpt++;
aaaaaaa aaa}
(3)
aaaafor ( i = 0; i < TX; i++ )
aaaaaaaaaamemcpy( NP[ i ],P[ i ], sizeof( int )*TY );

(4)aaaareturn cmpt;
aaaa}

(1) « init_feu » ne prend pas d’argument mais renvoie un entier.

(2) La boucle for permet de parcourir une colonne. La position horizontale i reste toujours la même, à 1 du bord, et toutes les positions verticales sont passées en revue. Pour chacune, si elle vaut BOIS elle passe à FEU et une variable compte tous les passages à FEU.

(3) Deuxième boucle dont la vocation est la recopie de la matrice P dans la matrice NP à l’aide de la fonction « memcpy ». La copie est faite ligne par ligne. Rappelons en effet que la première taille d’une matrice donne le nombre de lignes et la seconde le nombre de colonnes. Le nombre des lignes correspond ici à la taille horizontale et le nombre de colonne à la taille verticale. Mais nous aurions pu écrire également en une seule ligne :
aaaamemcpy( NP,P, sizeof( int )*TY *TX);

(4) Pour finir, le compte du nombre de feux est renvoyé à la fonction appelante.


3 Processus génératif

Fonction de propagation

Le feu se communique des cellules sur FEU aux cellules sur BOIS voisines. Les cellules sur FEU passent ensuite sur CENDRE. La fonction de propagation est la suivante :

(1)aaaavoid propagation(void)
aaaaaa{
(2)
aaaaint i, ia, ib, j, ja, jb;

(3)
aa aaaaafor ( i = 1; i < TX; i++ ) {
aaaaaaaaaaaaaia = ( i+1 )%TX;
aaaaaaaaaaaaaib = ( ( i -1 ) + TX )%TX;

(4)aaaaaaaaaafor ( j = 0; j < TY; j++ ) {
aaaaaaaaaaaaa aaja = ( j +1 )%TY;
aaaaaaaaaaaa aaajb = ( ( j -1 ) + TY )%TY;

(5)aaaaaaaaaaaaaif ( P[ i ][ j ]== BOIS ) {
aaaaaaaaaaaaaa aaaaif(a ( P[ ia ][ j ] == FEU ) || ( P[ i ][ jb ] == FEU ) ||
aaaaaaaaaaaaaaaaaaaa a( P[ ib ][ j ] == FEU ) || ( P[ i ][ ja ] == FEU ) ){

aaaaaaaaaaaaaaaaaaa aaNP[ i ][ j ] = FEU;
aaaaaaaaaaaaaaaaaaa aacmpt_feu++;
aaaaaaaaaaaaa aaaaa}
aaaaaaaaaaaaaaa}
aaaaaaaaaaaaaaaelse if ( P[ i ][ j ] == FEU ) {
aaaaaaaaaaaaaaaaaaaaaNP[ i ][ j ] = CENDRE;
aaaaaaaaaaaaaaaaaaa acmpt_feu--;
aaaaaaaaaa a aaa}
aaaaaaaa aaaa}
aaaaaaaaaa}
(6)
aa aaaaafor( i = 0; i < TX; i++ )
aaaaaaaaaaaaamemcpy(P[ i ], NP[ i ], sizeof( int )*TY );
aaaa}

(1) « propagation » ne prend pas d’argument et ne renvoie rien.

(2) Outre les variables i et j utilitaires pour passer en revue les positions de la matrice avec les boucles for imbriquées, quatre autres variables entières vont servir à désigner les positions adjacentes (est, nord, ouest, sud) à la position ( i, j ) courante.

(3) Première boucle for qui passe en revue toutes les lignes de la matrice. Les positions adjacentes ( i + 1 ) et ( i – 1 ) sont conservées respectivement dans les variable « ia » et « ib ». C’est surtout pour une question de clarté du code. Notons que nous optons pour un écran circulaire, c’est-à-dire que le débordement d’un côté se retrouve de l’autre côté.
Lorsque ( i + 1 ) déborde à droite de l’écran le reste de la division par la taille horizontale de la matrice ramène ce voisin de i à celui tout à gauche, et sans débordement ça ne change rien.
Lorsque ( i – 1 ) déborde à gauche de la matrice il faut d’abord rajouter la taille horizontale de la matrice avant d’opérer le modulo avec la taille horizontale de la matrice, ce qui renvoie complètement à droite de l’écran. De même, ça ne change rien si ( i – 1 ) ne sort pas de l’écran.

(4) Deuxième boucle for, imbriquée dans la première, avec cette fois les positions adjacentes verticales stockées respectivement dans ja et jb pour vers le bas et vers le haut. Le principe de contrôle est le même que pour la position horizontale, mais cette fois c’est la hauteur TY qui est utilisée.

(5) Au centre de l’imbrication des deux boucles, le test pour connaître la valeur de la position courante ( i, j ) si sa valeur est BOIS un second test est effectué pour connaître si une cellule voisine est sur FEU. Si oui, la cellule de la position courante passe sur FEU à la même position dans la matrice miroir NP qui accueille les résultats. Ensuite le compteur de feux est incrémenté de un.
En revanche si la position courante n’est pas sur BOIS mais sur FEU alors elle prend la valeur de CENDRE dans la matrice miroir NP et le compteur est diminué de un.

(6) Une fois que toutes les positions ont été regardées et les valeurs modifiées sauvées dans la matrice miroir NP il faut recopier les résultats obtenus de la matrice NP vers la matrice P afin de recommencer le processus avec la nouvelle configuration. La recopie est faite ligne par ligne et avec la fonction memcpy.

Boucle du processus

La boucle du processus, à placer dans la fonction « main » après les initialisations d’usage, est la suivante :

aaa aaa(…)

(1)aaaacmpt_feu=0;
(2)
aaaawhile ( ! key[KEY_ESC] ){

(3)aaaaaaaif ( key[KEY_ENTER]){
aaaaaa aaaaaainit_bois();
aaaaaaaaaa aacmpt_feu = init_feu();
aaaaaaa aa}
(4)
aaaaaaaif(cmpt_feu){
aaaaaaaa aaaapropagation();
aaaaaaaaaa aaaffiche_plan(page);
aaaaaaaaa aaablit(page,screen, 0,0,0,0, ECRAN_X,ECRAN_Y);
aaaaaaa aa}

aaaaaaa(…)


(1) La variable globale pour le comptage des feux est initialisée à 0.

(2) Comme souvent c’est une boucle while qui se termine avec la pression de la touche échap du clavier de l’ordinateur. Dans cette boucle il y a deux tests.

(3) Le premier test a pour objectif de permettre des initialisations et de relancer l’action lorsqu’un feu est fini. Il porte sur la pression de la touche « entrée ». Si oui la première fonction appelée est init_bois pour l’initialisation de la forêt. Ensuite c’est la fonction « init_feu » et le nombre de feux allumés renvoyés est affecté à la variable « cmpt_feu ».

(4) Si la variable cmpt_feu est différente de zéro, c’est-à-dire s’il y a des feux allumés, la propagation a lieu. Le calcul est fait une fois et la matrice P modifiée est affichée dans la bitmap intermédiaire « page » . C’est le rôle de la fonction « affiche_plan » vue plus haut. Enfin la bitmap « page » est affichée à l’écran. Lorsqu’il n’y a plus de feu, l’action s’arrête sur la dernière image et peut être relancée avec une nouvelle initialisation en pressant la touche entrée.

Fonction d’affichage

La fonction « affiche_plan » prend une bitmap de référence. Toutes les positions de la matrice P sont passées en revue et chacune d’elles est représentée par un rectangle de longueur pasx et hauteur pasy aux positions « i * pasx » et « j * pasy » définies par les macros X et Y. La couleur est celle de la position. Notons qu’il n’y a besoin que de trois couleurs, celle du fond, celle des bois, celle du feu et celle des cendres. Elles vont correspondre aux indices 1, 2, 3 d’une palette à créer avant la boucle.

aaaavoid affiche_plan(BITMAP *bmp)
aaaa{
aaaaint i, j;

aaaaaaaclear( bmp );
aaaaaaafor ( i = 0; i < TX; i++ )
aaaaaaaaaafor ( j = 0; j < TY; j++ )
aaaaaaaaaaaaarectfill( bmp, X( i ),Y( j ), X( i+1 ),Y( j+1 ), P[ i ][ j ] );
aaaa}