L-Systèmes : Morphogénèse des plantes November 13, 2010 Laurent Orseau <[email protected]> Antoine Cornuéjols <[email protected]> 1 Introduction La récursion, qui est le principe de base des langages de programmation fonctionnels tels Scheme, Lisp ou Caml, est au coeur de nombreux systèmes vivants (notamment la plupart des organismes pluri-cellulaires). En effet, la duplication est une forme de récursion puisqu’il s’agit de "faire la même chose". Cette duplication peut être identique ou différer selon certaines caractéristiques, comme lorsqu’une reine fourmi donne vie à différents types de fourmis. Il en va de même lors de la croissance d’organisme multicellulaires : les cellules se répliquent localement, pour donner naissances à de nouvelles cellules qui se répliqueront aussi et ainsi de suite. La récursion est en fait une forme plus générale que l’itération telle qu’on la voit dans les langages de programmation impératifs comme le C ou le Pascal, avec les boucles for, while, reapeat until, ... En effet, il est possible de faire toute itération en récursion, mais l’inverse n’est pas vrai. Les langages impératifs permettent d’ailleurs de faire de la récursion, mais ils ne sont généralement pas créés pour cela. Les itérations (seules) ne permettent pas de prendre en compte que les cellules qui se répliquent peuvent donner naissances à plusieurs cellules qui vont toutes refaire la même opération. Une itération ne fait qu’une seule chose à la fois. (Pour contourner ce problème, il est nécessaire de passer par une mémoire sous forme de tableau ou de liste.) En revanche, la modélisation d’un tel processus est direct en récursion, car c’est ce que fait exactement la récursion. Nous allons voir le lien très fort qui existe entre la morphogénèse des plantes et le principe de récursion. 1 2 L-Systèmes Le biologiste Aristid Lindenmayer (1926-1989) a modélisé la croissance de plantes par un tel processus récursif. Avec un outil graphique adéquat, il est possible de représenter des plantes qui semblent "naturelles". Une feuille d’érable. Une feuille générée par 10 lignes de Scheme. Une fougère générée par L-système (une quinzaine de lignes de Scheme). 2.1 Turtle Graphics Commençons pour définir le langage de dessin : nous allons recréer le langage LOGO. C’est un outil de dessin très simple à destination pédagogique, inventé par Seymourt 2 Papert en 1967. Il est très simple à utiliser, mais parce qu’il peut faire de la récursion, il peut générer des dessins très complexes. Le principe est de contrôler une "tortue" qui se déplace sur la fenêtre d’affichage en avançant d’une certaine distance, soit en laissant une marque au sol (tirer un trait) soit non, ou en tournant d’un certain angle. Une tortue est donc définie par sa position en x et en y ainsi que par son angle (par défaut le 0 est l’Est). Question 1 Commencez par créer une fenêtre graphique et affichez-la : (define monde (make-board 1 1 600 600)) (board-draw monde 0.1) Question 2 Définissons maintenant une tortue initiale. Créez une fonction : (make-tortue) → (list number? number? number?) qui renvoie une tortue, c’est-à-dire une liste contenant la position en x et en y ainsi que l’angle initial. Il est conseillé de placer la tortue vers (300, 400) mais vous pouvez et pourrez changer ces valeurs à votre convenance. Question 3 Comme dans les TP précédents, créez les accesseurs posx, posy et angle permettant d’extraire d’une tortue (une liste) la valeur correspondante. Question 4 Créez une fonction : (turn a t) → tortue? a : number? t : tortue? qui renvoie la tortue t tournée d’un angle a. Question 5 Créez une fonction : (move dist t) → tortue? dist : number? t : tortue? qui renvoie la tortue t déplacée d’une longueur dist dans la direction donnée par son angle. La tortue est déplacée sans rien dessiner. Attention, les fonctions prédéfinies cos et sin prennent des radians en entrée, mais il est plus confortable pour la suite du TP d’utiliser des degrés. Il vous faudra donc les convertir. Il est conseiller de créer une fonction intermédiaire : 3 (degres->radians deg) → number? deg : number? qui permet d’effectuer la conversion. Question 6 La fonction : (board-draw-line board x-src y-src x-dst y-dst) → void? board : board? x-src : number? y-src : number? x-dst : number? y-dst : number? permet de tracer à l’écran une ligne dans la fenêtre graphique, de x-src, y-src à x-dst, y-dst. Créez une fonction : (draw dist t) → tortue? dist : number t : tortue? similaire à move mais qui, en plus de renvoyer la nouvelle tortue, dessine un trait à l’écran de son ancienne position à la nouvelle. 2.2 Appels imbriqués Puisque chacune des fonctions définies prend une tortue et retourne la nouvelle tortue, il faut imbriquer les appels à ces fonctions pour générer des séquences de dessin. Par exemple, prenons l’appel : (draw 50 (turn 20 (draw 100 (make-tortue)))) En Scheme, ce sont les appels de fonction les plus profondément imbriqués qui sont exécutés en premier. Ici, on crée donc d’abord une tortue, puis on la fait avancer de 100 tout en traçant une ligne, puis on la fait tourner de 20 degrés, puis on la fait avancer de 50 en dessinant. Vous pouvez dors et déjà vous amuser un peu avec ces primitives graphiques pour bien les prendre en main. 3 Un premier exemple récursif Tout l’intérêt de ce système graphique réside dans la récursion. Définissez la fonction suivante : (define (escargot n tortue) (when (> n 1) (escargot (* n 0.97) (draw n (turn 15 tortue))))) 4 Evidemment, lisez-là et comprenez-là. Le when permet de n’exécuter une suite d’expressions que si la condition donnée est vérifiée : (when condition expression1 expression2 ... expression-fin) La valeur de retour du when est la valeur de expression-fin. À la fin de votre programme, exécutez un appel à cette fonction : (board-draw monde 0 (escargot 20 (make-turtle))) Notez que cet appel est à l’intérieur d’un appel à board-draw, qui permet de préparer la fenêtre pour le dessin et de faire les pré-traitements et post-traitements nécessaires. Modifiez la fonction escargot, essayez diverses choses, amusez-vous avec le langage graphique ! Un nautile 4 Quelques devinettes Question 7 Créez une fonction : (f1 n t) → void? n : number? t : tortue? qui dessine un trait de longueur n (en avançant la tortue), appelle f1 avec la moitié de n et la tortue tournée d’un angle de 90◦ vers la droite, puis appelle f1 avec la moitié de n et la tortue tournée d’un angle de 90◦ vers la gauche. La récursion doit s’arrêter lorsque n a une valeur inférieure à 1. Puis faites l’appel suivant : (f1 100 (make-tortue)) 5 Essayez ensuite de modifier les différentes valeurs (rapport et angle) pour voir le résultat. Question 8 Ecrivez une fonction : (f4 profondeur prof-max tortue) → void? profondeur : number? prof-max : number? tortue : tortue? qui arrête le calcul à une profondeur maximale donnée et génère une image comme sur les exemples suivants. Initialement, profondeur est toujours égal à 1. (f4 1 2 (make-turtle)) (f4 1 5 (make-turtle)) (f4 1 10 (make-turtle)) Question 9 Trouvez la fonction qui génère la courbe suivante : Le triangle de Sierpinski. Ou presque. Indice : la tortue est partie du centre. Essayez de voir ce qu’elle a pu faire dans le premier appel de la fonction... Question 10 Trouvez la fonction qui génère la courbe suivante : 6 4.1 En Technicolor Pour ajouter un peu d’épaisseur et de couleurs à vos traits, vous pouvez utiliser les fonctions suivantes : (board-set-pen-color board r g b) → void? board : board? r : integer? g : integer? b : integer? Change la couleur courante du trait pour celle donnée en Red-Green-Blue. Les valeurs sont des nombres entiers de 0 à 255 inclus. Attention à ne pas dépasser de ces bornes. Vous pouvez paramétrer ces valeurs selon le paramètre de profondeur ou la longueur du trait. Par exemple : (board-set-pen-color monde 0 255 0) changera la couleur pour du vert. La fonction : (board-set-pen-width board width) → void? board : board? width : number? Modifie la taille du trait. 4.2 Visualisation en direct live Certaines figures peuvent prendre un certain temps avant d’apparaître à l’écran. Pour voir le développement en direct sans que cela prenne trop de temps sur le temps de calcul, on peut utiliser la technique suivante : Par exemple, pour la fonction f1, les deux lignes du premier when ont été ajoutée pour mettre le monde à jour uniquement lorsque n > 2 : (define (f1 n tortue) (when (> n 2) 7 (board-redraw monde)) ....) En effet, puisque f1 s’apelle récursivement 2 fois, il y a une explosion combinatoire du nombre d’appels. Plus n est petit, plus le nombre d’exécutions de f1 est grand. En forçant l’affichage uniquement pour des "grandes" valeurs de n, cet affichage ne sera fait que peu de fois (et ne prendra donc pas beaucoup de temps de calcul). 5 À vous de jouer ! Concours de celui qui fera la plus belle création ! Envoyez-moi le code de vos trouvailles par email ! 8