Programmation Fonctionnelle, TP2

Figure
Version & licenses
Creative Commons License

Programmation Fonctionnelle, TP1 : Corriger du code avec QCheck.

Guyslain Naves
Zoom sur une partie de la fractale de Mandelbrot.

Le but de ce TP est de corriger un programme contenant plusieurs erreurs à l'aide de QCheck. Le programme doit générer des images de la fractale de Mandelbrot. Le programme est contenu dans cette archive :

le programme.

Récupérez-le et extrayez l'archive dans un répertoire dédié. Le projet vient tout prêt avec ses fichiers .merlin et _tags.

Compilez mandelbrot.native et lancez le programme. Décevant, n'est-ce pas ?

Cplex

La fractale de Mandelbrot représente la convergence d'une suite complexe définie selon un paramètre, nous verrons les détails plus tard, mais dans l'immédiat il nous faut faire des calculs sur les nombres complexes, et c'est le rôle du module Cplex. Cplex n'a pas de dépendances dans le projet, c'est donc un bon candidat pour commencer les corrections.

Lisez l'interface du module (le fichier cplex.mli). Assurez-vous de bien comprendre le rôle de chaque fonction. arbitrary est un générateur aléatoire de nombres complexes qui sera utilisable par QCheck.

Créez un nouveau fichier checkCplex.ml, qui va contenir le code pour tester le module Cplex. Puisqu'on va utiliser Cplex abondamment, autant l'importer avec open Cplex au début du fichier.

Pour commencer, on va vérifier que l'addition est correcte. L'addition complexe vérifie les mêmes lois que l'addition réelle : neutralité de zéro, associativité, commutativité, existence d'un inverse. Cela fait déjà quelques candidats pour l'écriture de tests. Par exemple :

  1. open Cplex

  2. let law_neutrality_of_zero complex =
  3. are_almost_equal (complex +: zero) complex

  4. let test_zero_neutrality =
  5. QCheck.Test.make
  6. ~count:100
  7. ~max_fail:10
  8. ~name:"Zero is neutral for the addition"
  9. Cplex.arbitrary
  10. law_neutrality_of_zero


  11. (* The main function: simply add new tests to the list, then evaluate
  12. the file. *)
  13. let run_all_tests =
  14. try
  15. List.iter QCheck.Test.check_exn
  16. [
  17. test_zero_neutrality;
  18. (* test_associativity; *)
  19. ];
  20. Printf.printf "OK.\n"
  21. with
  22. | QCheck.Test.Test_error (name, instance, excep) ->
  23. Printf.printf "%s: FAIL, exception raised on %s.\n%!" name instance;
  24. raise excep
  25. | QCheck.Test.Test_fail (name, counterexamples) ->
  26. Printf.printf "%s: FAIL, counterexamples:\n%!" name;
  27. List.iter (Printf.printf "%s\n%!") counterexamples

Compilez checkCplex et exécutez-le. Si rien n'est écrit, le test est passé avec succès.

Ajoutez un test pour l'associativité. Le test doit prendre un triplet de complexes en argument. Comment construire un générateur aléatoire de triplets de complexes ? cherchez ici.

Ajoutez un test pour la commutativité. Cette fois il faut une paire de complexes, cela ne devrait pas être bien dur si vous avez compris.

Imaginez deux tests, pour negate et (-:) et ajoutez-les.

Ajouter un test pour vérifier que l'addition se comporte correctement sur des complexes sans partie imaginaire (l'addition doit coïncider avec l'addition des flottants).

Si vous avez trouver une erreur, corriger le code de l'addition, jusqu'à ce que tous les tests passent. N'oubliez pas que si vous détectez une erreur, elle peut être dans le code testée (c'est le but) mais elle peut aussi être dans le code testant (c'est ballot) ! Pour examiner une erreur, vous pouvez ouvrir un interpréteur et effectuer directement des calculs pour essayer de comprendre pourquoi ce n'est pas correct.

  1. #use "topfind";;
  2. #require "qcheck";;
  3. #directory "_build";;
  4. #load "cplex.cmo";;

Avec tous ces tests, on commence à avoir confiance dans la qualité du code de l'addition. On recommence maintenant pour la multiplication. Ajoutez des tests pour l'associativité et la commutativité de la multiplication, la distributivité de la multiplication sur l'addition, la neutralité de 1. Testez aussi l'inverse.

Les nombres complexes représentent des transformations géométriques : multiplier par un réel pur, c'est grandir ou rétrécir un objet, multiplier par un point du cercle unité (de centre 0, de rayon 1), c'est effectuer une rotation. On trouve donc une fonction créant un complexe représentant une rotation d'un angle donné en argument (en radian). On définit :

  1. let pi = Gg.Float.pi

Proposez des tests pour la rotation. On peut utiliser par exemple que composer deux rotations d'angle $\alpha$ et $\beta$ fait une rotation d'angles $\alpha + \beta$, ou que $i$ correspond à la rotation d'un quart de tour. Commencez donc par écrire un générateur aléatoire d'angles entre $0$ et $2\pi$.

Finalement testez la norme. Elle est invariante par rotation, et la norme d'un réel pur est sa valeur absolue. Utiliser Cplex.compare_float pour tester l'égalité des flottants.

Une fois le module Cplex corrigé, réessayez d'exécuter le programme principal. Est-ce mieux ?

Les autres modules.

range

Il reste d'autres erreurs deci-delà. On commence par la fonction range de MoreList, qui permet de générer l'intervalle de tous les entiers entre deux bornes (incluses)

  1. range 3 8 = [3;4;5;6;7;8] (* expected *)

Pour la tester, il faut un générateur d'instances intervalles. Pour cela, il faut :

  • un générateur aléatoire QCheck.Gen.t,
  • une fonction de conversion en chaînes de caractères,
  • une fonction de simplification QCheck.shrink.

Pour le générateur aléatoire, utilisez pair, -- et map (voir la doc), pour générer la valeur minimum de l'intervalle (disons entre $-1000$ et $1000$), la longueur (entre 0 et 100), et en déduire une paire (borne inférieure, borne supérieure).

Pour la conversion, utilisez Printf.printf.

Pour la simplification, utilisez QCheck.Iter. Voici deux règles de simplification possibles. Si la longueur est strictement positive, essayez de la diviser par deux en diminuant la borne supérieure. Si la borne inférieure est non-nulle, essayez de la diviser par deux ainsi que la borne supérieure.

Ensuite, écrivez des tests pour range (par exemple, tester les extrémités de la liste, et sa longueur).

repeat

Le module Sequence permet d'effectuer des calculs sur des suites d'entiers définies par une récursion simple ($u_0 = c$, $u_n = f(u_{n-1})$). L'ensemble de Mandelbrot est l'ensemble des points complexes $c$ tels que la suite définie avec $u_0 = 0$, $f : z \to z^2 + c$ converge.

Sequence.first_term calcule le premier terme de la suite satisfaisant une condition donnée. Cette fonction se base sur repeat et repeat_until qui permettent d'itérer une fonction à partir d'une valeur initiale, soit $n$ fois, soit jusqu'à la réalisation d'une condition.

Proposez des tests pour repeat et repeat_until. Pour générer des fonctions aléatoires, utilisez QCheck.(fun1 small_int small_int).

View

Le module View permet de définir le cadrage de l'image : on affiche seulement un rectangle du plan complexe. Pour chaque pixel, on trouve un point complexe correspondant, on calcule la suite complexe et on détermine si elle diverge. complex_of_pixel est la fonction permettant de calculer le point complexe associé à un pixel, par rapport au cadrage donné.

Malheureusement, elle contient une erreur. Proposez un test capable de détecter l'erreur, et corrigez-là.

Réessayez d'exécuter le programme mandelbrot. Cette fois, le résultat devrait être l'image en haut de cette page. Sinon, continuez de tester, il vous manque des erreurs.

Amusez-vous...

Le principe du dessin est de calculer pour chaque point le $n$e terme (disons $n = 100$) de la suite définie par sa fonction polynomiale associée. Si la suite diverge, la norme de ses termes diverge aussi. On classe alors les pixels selon la norme du $n$e terme, et on leur associe une couleur dans cet ordre.

Pour accélérer, on ne calcule pas nécessairement tous les $n$ termes, on s'arrête lorsque la norme est assez grande, et cela suffit pour estimer à quel point la divergence est rapide, et trier les pixels. On laisse en noir les pixels pour lesquels la suite converge (ou semble converger).

Le programme peut facilement être personnalisé. Voici quelques défis :

  • changer la vue, la taille de l'image,
  • changer les couleurs,
  • changer le polynome utilisé,
  • ajouter des fonctionnalités (déplacement avec la souris ou le clavier, suréchantillonage pour améliorer l'image,...),
  • améliorer les performances.

Lisez le code, comprenez-le et appropriez-vous-le.