TP n°2: Automates cellulaires à une dimension

Partie pratique

Voir ici l'article wikipédia

Voir ici l'article cellular automaton du site Mathworld

Elementary cellular automaton

Avant de commencer à programmer un automate cellulaire vous allez en utilisant un plateau quadrillé et des jetons en plastique vous familiariser avec un automate cellulaire à une dimension.

Un automate cellulaire à une dimension est une suite de cases juxtaposées en ligne (une dimension) représentant des cellules ayant deux états possibles: soit la cellule est vivante la case est donc occupée par un jeton en plastique. Soit la case est vide et il n'y a pas de cellule.

Ensuite les cellules évoluent suivant des règles d'évolution, tenant compte des voisins , à gauche et à droite de la case étudiée. Prenons par exemple la règle 90 (voir l'image ci-dessous), on voit qu'une cellule reste vivante à la génération suivante si elle n'a qu'un voisin (à gauche ou à droite). Dans les autres cas elle meurt à l'état suivant.

On voit à l'état initial une colonie ayant deux cellules. A l'instant suivant (Etat 1) la colonie a évolué en donnant quatre cellules.

Exercice

  1. Avez vous bien compris pourquoi on appelle cette règle la règle 90 ?
  2. Continuer de faire évoluer la colonie sur les lignes suivantes.
  3. Changer d'état initial :partir d'une seule cellule, en gardant la même règle 90.
  4. Changer de règle:Utiliser la règle 30 puis la règle 122 et 126.

Programmation

Nous allons représenter l'état actuel d'une colonie par un tableau de longueur 100 nommé etatActuel contenant que des 0 ou des 1. Puis nous allons construire l'état suivant de la colonie dans un autre tableau nommé etatSuivant. La règle en cours est un tableau de longueur 8 nommé regle. Au début la colonie ne contiendra qu'une cellule. L'initialisation sera donc etatActuel[50] = 1. Nous allons vérifier les images données dans l'article Elementary Cellular Automaton.

Recopier le code suivant le compléter le compiler et l'exécuter.


class automateDim1 { 
public static void main (String[] args) {

		// déclaration
		int[] regle = new int[8];
		int[] etatActuel = new int[100];
		int[] etatSuivant = new int[100];
		int num;
		// Entrée des données
		// Entrée de la règle 90
		regle[0] = 0;
		regle[1] = 1;
		regle[2] = 0;
		regle[3] = 1;
		regle[4] = 1;
		regle[5] = 0;
		regle[6] = 1;
		regle[7] = 0;
		//Entrée de la colonie à l'état initial
		etatActuel[50] = 1;
		
		// On suit l'évolution de la colonie sur une vingtaine de générations
		// La variable num numérote le nombre de générations
		
		for(.............................){
		
		// Hérédité: Construction de etatSuivant[i] à partir de etatActuel[i]  
		//on parcourt le tableau etatActuel du deuxième élément à l'avant dernier 
		//et on regarde pour chaque élément e = etatActuel[i] 
		//les voisins a = etatActuel[i-1] et b = etatActuel[i+1] de cet élément
		//aeb forme un nombre entier en binaire on convertit ce nombre en décimal d. 
		//Ce nombre d est l'indice dans le tableau regle de l'état suivant de la case repérée par i
		//d = etatSuivant[i]
					
					
					for(................){
								......................
					}
					//A la sortie de la boucle etatSuivant a été construit 
					//Sortie graphique: On souhaite visualiser à l'écran étatSuivant
		
		
		
		
					//On recommence le processus etatSuivant joue le rôle d'etatActuel
					//
					.................................................................
		}
		
}
}

Voir la correction ici

Il s'agit maintenant d'apporter des modifications au programme précédent.

Exercices

  1. On voudrait pouvoir entrer le numéro de la règle sous forme d'un entier sous forme décimal qui sera ensuite convertit en binaire et inséré dans le tableau regle.(voir manuel )
  2. On aimerait que le dessin des règles soit "plus joli" et se fasse dans une fenêtre en dehors du terminal.

Graphisme et fonctions

Les langages comme Java sont livrés avec une panoplie d'outils déjà prêts à l'emploi. On parle d' A.P.I c'est à dire Application Programming Interface. Il existe ainsi des classes pour le graphisme et on trouve sur le Web des tutoriaux qui nous renseignent sur ces classes. Regardons à l'intérieur de la classe Isn et au début de cette classe nous voyons ces lignes import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D;Ces classes livrées avec le JDK (Java Development Kit) sont appelées pour être utilisées avec le mot import. Sinon le compilateur ne reconnaitra pas les outils utilisés.

Pour commencer à dessiner en dehors du terminal il faut ouvrir une autre fenêtre et il y a une classe qui gère la manipulation des fenêtres c'est la classe swing. On voit que ces fonctions sont utilisées dans la classe Isn. Vous pouvez voir ce qui suit dans la classe Isn, c'est une FONCTION , qui comme une fonction mathématique réalise une tâche bien précise d'ouvrir une fenêtre d'une certaine taille, à un certain endroit de l'écran et avec un titre.


public static void initDrawing (String s, int x, int y, int w, int h) {
    component = new JIsn(w,h);
    component.setOpaque(true);
    component.setBackground(Color.WHITE);
    JFrame frame = new JFrame(s);
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    frame.add(component);
    frame.setLocation(x,y);
    frame.pack();
    frame.setVisible(true);}

Avant de plonger plus avant il s'agit de regarder comment on se repère sur un écran: user space .

Donc la fonction ci-dessus ouvre à l'écran une fenêtre repérée sur l'écran par le coin supérieur gauche (x,y), la largeur w de la fenêtre (width) et la hauteur h de la fenêtre (height). Nous allons maintenant utiliser cette fonction dans un programme pour se familiariser avec elle. Recopier le code suivant et compiler le puis exécuter le.


class fenetre{ 
public static void main (String[] args) {
while(true){
		System.out.println("Que mettre dans la chaîne s ?");
		String s  = Isn.readString();
		System.out.println("où placer le coin supérieur gauche de la fenêtre  ?");
		System.out.println("Abscisse?");
		int x = Isn.readInt();
		System.out.println("Ordonnée?");
		int y = Isn.readInt();
		System.out.println("largeur de la fenêtre?");
		int w = Isn.readInt();
		System.out.println("hauteur de la fenêtre?");
		int h = Isn.readInt();
		initDrawing (s,x,y,w,h);
		}
}		
}
    

Après que nous ayons ouvert une fenêtre il nous faut dessiner à l'intérieur, continuons de regarder à l'intérieur de la classe Isn ce qu'il y a. On trouve au moins ces quatre fonctions:


public static void drawPixel(double x,double y,int c1,int c2,int c3) {
    component.add(new Rectangle2D.Double(x,y,0,0), 
                  new Color (c1,c2,c3),Color.WHITE);}

  public static void drawRect(double x,double y,double a, double b, int c1,
                              int c2,int c3) {
    component.add(new Rectangle2D.Double(x,y,a,b), new Color (c1,c2,c3),  
                  Color.WHITE);}

  public static void paintRect(double x,double y,double a, double b, int c1,
                               int c2,int c3) {
    component.add(new Rectangle2D.Double(x,y,a,b), new Color (c1,c2,c3),
                  new Color (c1,c2,c3));}

  public static void drawLine(double x1,double y1,double x2, double y2, int c1,
                              int c2, int c3) {
    component.add(new Line2D.Double(x1, y1, x2, y2),new Color (c1,c2,c3),
                  Color.WHITE);}



Elles commencent toutes par public ( pour préciser que ces fonctions sont utilisables par tout le monde à l'extérieur de la classe) puis static (on verra plus tard) void (ces fonctions ne retournent pas quelque chose , un nombre, une réference mais réalisent une action)et leur nom relativement explicite ici.

Entre parenthèses on a les paramètres de la fonction en gros ce qu'il faut mettre dans la fonction pour qu'elle réalise sa tâche

Le nom aussi explicite soit il (attention aux contre-sens) ne suffit pas à comprendre une fonction. Il faut plonger à l'intérieur du code.

On constate que ces fonctions appellent une autre fonction spéciale appelée constructeur, qui cette fois ci n'est pas static la fonction Rectangle2D.Double. Que dit la documentation sur cette fonction ?


public Rectangle2D.Double(double x,
                  double y,
                  double w,
                  double h)

Constructs and initializes a Rectangle2D from the specified double coordinates.

Parameters:
    x - the X coordinate of the upper-left corner of the newly constructed Rectangle2D
    y - the Y coordinate of the upper-left corner of the newly constructed Rectangle2D
    w - the width of the newly constructed Rectangle2D
    h - the height of the newly constructed Rectangle2D
    
    

Autrement dit les paramètres a et b dans drawRect et paintRect sont respectivement des largeur et hauteur. Testons ces fonctions. Copiez le code suivant et exécutez le:


class dessinePixel{ 
public static void main (String[] args) {

		Isn.initDrawing ("dessine moi un pixel",100,100,300,500);
		//dessine un pixel rouge dans le coin supérieur gauche à l'intérieur de la fenêtre
		Isn.drawPixel(0,0,255,0,0);
		//dessine un pixel vert dans le coin inférieur droit à l'intérieur de la fenêtre
		Isn.drawPixel(300,500,0,255,0);
		//dessine un pixel bleu dans le coin supérieur droit à l'intérieur de la fenêtre
		//tirer sur la fenêtre pour voir les pixels sur le bord
		Isn.drawPixel(300,0,0,0,255);
		}
}		

    

Etudions la fonction dessiner un rectangle.


class dessineRectangle{ 
public static void main (String[] args) {

		Isn.initDrawing ("dessine moi un carre de bord rouge",100,100,300,300);
		//dessine un carre de bord rouge rouge dans le coin supérieur gauche à l'intérieur de la fenêtre
		Isn.drawRect(0,0,150,150,255,0,0);
		}
}

Constatez que l'on a mis des entiers à la place de double et la fonction a marché quand même! A vous maintenant de tester paintRect en adaptant le programme ci-dessus. Que se passe-t-il? Le voit-on dans le code? Quelle est la logique ?

N'oublions pas notre TP : c'est à dire tracer une grille dans une fenêtre et peindre chaque case si elle est occupée par une cellule. Pour tracer des droites nous avons la fonction drawLine où les paramètres essentiels sont (x1,y1) et (x2,y2) les coordonnées des points à l'extrémité de la ligne. Nous allons tracer des droites horizontales rouges régulièrement tous les 10 pixels dans une fenêtre de largeur 1000 et de hauteur 200.


class dessineDroite{ 
public static void main (String[] args) {

		Isn.initDrawing ("dessine moi des droites",100,100,1000,200);
		for(int i = 1; i <= 19; i = i+1){
			Isn.drawLine(0,10*i,1000,10*i,255,0,0);
		}
}
}

Exercice

  1. Adaptez le programme précédent pour avoir en plus des droites verticales espacées par 10 pixels. Toutes les droites doivent être noires.
  2. En repérant chaque cellule par son coin supérieur gauche, peindre en rouge une droite horizontale ou verticale formée de cellules de la fenêtre.
  3. Vous pouvez maintenant modifier la classe automateDim1 pour que l'évolution de la colonie se fasse en dehors du terminal.

Notion de fonction et d'algorithme.

Le programme principal main a tendance à grossir au fur et à mesure que l'on veut rajouter des modifications. Le risque étant que :

  1. Le programme perd en clarté et lisibilité
  2. Le programme perd en fiabilité: Comment être sûr d'avoir anticipé toutes les conséquences des modifications sur le programme?

Déjà chez le philosophe français René Descartes on trouve une méthode d' analyse :

"...au lieu de ce grand nombre de préceptes dont la logique est composée, je crus que j'aurais assez des quatre suivants, pourvu que je pris une ferme et constante résolution de ne manquer pas une seule fois à les observer. Le premier était de ne recevoir jamais aucune chose pour vraie que je ne la connusse évidemment être telle; c'est à dire d'éviter soigneusement la précipitation et la prévention; et de ne comprendre rien de plus en mes jugements que ce qui se présenterait si clairement et si distinctement à mon esprit que je n'eusse aucune occasion de le mettre en doute. Le second, de :

diviser chacune des difficultés que j'examinerais en autant de parcelles qu'il se pourrait et qu'il serait requis pour les mieux résoudre.

Le troisième, de conduire par ordre mes pensées, en commençant par les objets les plus simples et les plus aisés à connaître, pour monter peu à peu, comme par degrés, jusques à la connaissance des plus composés; et supposant même de l'ordre entre ceux qui ne se précédent point naturellement les uns les autres. Et le dernier, de faire partout des dénombrements si entiers, et des revues si générales, que je fusse assuré de ne rien omettre." (Discours de la méthode).

Nous allons donc décomposer le programme principal le "main" en "autant de parcelles qu'il se pourrait et qu'il serait requis pour les mieux résoudre". Nous allons aussi écrire dans un premier temps l'algorithme sous-jacent au programme pour ne pas se focaliser sur l'aspect technique mais sur la vision globale du problème. Autrement dit dans un premier temps nous décomposons notre problème en sous-problèmes sans chercher à résoudre pour l'instant les sous-problèmes; on ne regarde que l'aspect global. Nous obtenons donc:

    

		//Programme principal
		
		Demander à l'utilisateur le numéro i (en décimal de 2 à 255) de la règle
		affecterRegle(i)
		affecter à etatActuel  une colonie à une seule cellule
		creerFenetre
		dessinerGrille
		peindreColonie(etatActuel)
		Pour i allant de 1 à 20 faire
			etatSuivant prend la référence de  generationEtatSuivant(etatActuel)
			peindreColonie(etatSuivant)
			echange de références entre etatActuel et etatSuivant
		FinPour
    
    

Nous pouvons maintenant mettre au point séparément chaque "parcelle" et ceci pour toutes les parcelles.

Une fois assuré qu'elles fonctionnent on va les réunir, pas avant.

Au lieu de mettre au point un programme principal de la première ligne à la dernière , et ceci pour un programme principal de plus en plus grand, ce qui est difficile lorsque cela ne marche pas, nous mettrons au point des petites unités d'abord, qui seront plus facilement réutilisables par la suite.

En vous aidant de votre manuel p53 à 62 créer une classe test pour tester séparément chaque fonction.

Exercice

  1. Ecrire la fonction en Java affecterRegle . Vous pouvez vous inspirer du programme suivant:
       
       class baseDeux{
       /**********************************
       *
       *
       Ce programme convertit un entier compris entre 0 et 255 en un 
       nombre en binaire rangé dans un tableau de taille 8, puis affiche la conversion
       *
       *
       ************************************/
    	public static void main(String[]args){
    	
    		int[] bin = new int[8];
    		System.out.println("Entrez un nombre décimal");
    		int nombredec = Isn.readInt();
    		int quotient = nombredec;
    		int indice = 0;
    		int reste;
    		//Tant que le quotient n'est pas nul on divise le quotient par deux
    		//Au départ le quotient est l'entier décimal
    		while(quotient > 0){
    			reste = quotient % 2;
    			bin[indice] = reste;
    			quotient = quotient/2;
    			indice = indice + 1;
    		}
    		//on complète jusqu'à l'indice 7 par des 0 s'il le faut
    		for(int i = indice;i <= 7;i = i+1){
    			bin[i] = 0;
    		}
    		
    		for(int i = 7;i >= 0;i = i-1){
    			System.out.print(nombredec+"  en binaire vaut:  ");
    			System.out.print(regle[i]);
    			System.out.println;
    		}
    		
    	}
    }
    
    
  2. Ecrire une fonction creerFenetre et la tester seule.
  3. Ecrire une fonction dessinerGrille et la tester à part.
  4. Ecrire une fonction peindreColonie et la tester à part.

    Pour mettre au point la fonction la tester sur les colonies suivantes:

    1. la colonie vide de 0 à 99
    2. la colonie à une seule cellule
    3. la colonie ayant une cellule sur deux
    4. la colonie ayant 100 cellules
  5. Ecrire une fonction generationEtatSuivant et la tester à partir de la colonie à une seule cellule sur une ou deux générations Puis sur plusieurs générations. Ne pas hésiter à afficher le contenu de certaines variables critiques si la fonction ne fonctionne pas. Ne pas oublier les extrémités de la colonie avec les règles 3, 5, 7 etc...qui se terminent par 1 en binaire.
  6. Est il possible d'écrire une fonction "simple" echanger qui échange les rôles etatActuel et etatSuivant. (Voir page 72 et 74 du manuel)
  7. Ecrire la classe automateDim1Fonction avec toutes les modifications graphiques et les fonctions, la tester avec l'article Elementary Cellular Automaton sur:
    1. la règle 220
    2. la règle 250
    3. et quelques règles qui se terminent par 1 en binaire comme 3, 5, 7, etc...

Voir la correction ici

Idées de projets :

Indications et images

Voici une image pour mieux comprendre comment sont construit les droites et comment sont peints les rectangles

Voici quelques images

La règle 22 sur 500 générations avec une cellule au départ

La règle 22 sur 200 générations avec initialisation aléatoire

La règle 22 sur 200 générations avec une seule cellule au départ et un taux de régénération de 70 %

La règle 22 sur 200 générations avec une seule cellule au départ et un taux de régénération de 75 %

La règle 22 sur 200 générations avec une seule cellule au départ et un taux de régénération de 80 %

La règle 195 sur 200 générations avec une cellule au départ

La règle 165 sur 200 générations avec une cellule au départ

La règle 225 sur 200 générations avec une cellule au départ

La règle 110 sur 700 générations avec une cellule au départ

La règle 948 sur 400 générations avec une cellule grise au départ (automate totalistique à 3 états)