par Nicolas Martignoni Collège Sainte-Croix 1700 Fribourg (Suisse) Version 2.0 © Copyright 1992-1998 Nicolas Martignoni. Tous droits réservés.
Nous allons traiter dans ce chapitre de certains aspects plus avancés de Mathematica, plus particulièrement les techniques de programmation ainsi que la manipulation des expressions et des listes.
Mathematica manipule un très grand nombre d'objets différents: des formules mathématiques, des listes, des nombres, des graphiques, etc. Bien que ces objets aient l'air très différents, Mathematica se les représente tous de la même façon: ce sont tous des expressions. Les expressions Mathematica sont toutes de la forme f[a, b, ...].
Il n'est cependant pas nécessaire d'écrire ces expressions explicitement sous cette forme. Par exemple, x + y est aussi une expression. Quand dans le front end on tape x + y, le kernel convertit cette expression en sa forme complète Plus[x, y]. Le front end l'affiche automatiquement sous la forme x + y.
La même règle est vraie pour tous les opérateurs, tels ^ et /. Pour voir la forme complète d'une expression, on utilise la fonction FullForm.
FullForm[x y z]
Times[x, y, z]
FullForm[{x, y, z}]
List[x, y, z]
FullForm[a -> b]
Rule[a, b]
FullForm[xn]
Power[x, n]
Cela fonctionne aussi pour les expressions plus compliquées:
e = x2 + 3 - (y + z)3;
FullForm[e]
Plus[3, Power[x, 2], Times[-1, Power[Plus[y, z], 3]]]
Dans une expression f[a, b, ...], l'objet f est appelé le head (ou la tête) de l'expression. Il est toujours possible d'extraire le head d'une expression:
Head[xn]
Power
Head[12354]
Integer
Head[2 + 3 I]
Complex
Trouvez la forme complète (FullForm) de l'expression suivante sans l'aide de Mathematica.
![]()
Les doubles crochets ou leurs équivalents
et
ne servent pas seulement à extraire les éléments d'une liste, mais de n'importe quelle expression. On peut ainsi tirer des sous-expressions assez facilement.
e[[3]]
-(y + z)3
FullForm[%]
Times[-1, Power[Plus[y, z], 3]]
Le troisième élément de l'expression Plus[...] est bien l'expression -(y + z)3. On peut continuer de la même façon. La commande suivante montre que le deuxième élément du Times[...] est (y + z)3.
e[[3, 2]]
(y + z)3
Clear[e]
Quelle commande faut-il taper pour tirer le -3 x de l'expression donnée à l'exercice précédent?
Le concept d'expression permet de nombreuses opérations, grâce à sa souplesse. Il est tout d'abord possible d'appliquer une fonction quelconque à n'importe quelle expression, à l'aide de la fonction Apply.
Apply[f, {a, b, c}]
f[a, b, c]
Apply[Plus, {a, b, c}]
a + b + c
En fait cette fonction ne fait que remplacer le head de l'expression par celui que l'on spécifie comme premier argument. On se souvient que la forme complète de {a, b, c} est List[a, b, c]. Dans les exemples ci-dessus, List a été remplacé dans le premier par f, et dans le deuxième par Plus. On a ainsi obtenu dans le premier cas f[a, b, c], et dans le second, Plus[a, b, c], qui a été affiché dans sa forme standard a + b + c par le front end.
Connaissant ce principe, il est facile d'effectuer l'opération inverse de celle qu'on vient de faire, c'est-à-dire remplacer Plus par List:
Apply[List, a + b + c]
{a, b, c}
Fabriquez à l'aide d'Apply une fonction qui additionne les n premiers nombres naturels.
Indication: utilisez la fonction Range.
Il est parfois nécessaire d'appliquer une fonction non pas à une expression entière, mais à des parties de celle-ci. Si l'on écrit f[{a, b, c}], on applique la fonction f sur l'ensemble de la liste. Pour appliquer au contraire la fonction séparément sur chaque élément de la liste, on utilise la fonction Map.
f[{a, b, c}]
f[{a, b, c}]
Map[f, {a, b, c}]
{f[a], f[b], f[c]}
L'effet de Map est de «distribuer» la fonction f à l'intérieur de la liste. La plupart des fonctions mathématiques prédéfinies Mathematica adoptent ce comportement automatiquement. C'est cela qui rend si puissant le travail sur les listes.
Plus[3, {a, b, c}]
{3 + a, 3 + b, 3 + c}
L'expression ci-dessus montre ce comportement avec la fonction Plus. On l'écrit d'habitude:
3 + {a, b, c}
{3 + a, 3 + b, 3 + c}
Voici un autre exemple avec la fonction Log.
Log[{a, b, c}]
{Log[a], Log[b], Log[c]}
Map ne fonctionne pas seulement avec les listes, mais avec n'importe quelle expression, c'est-à-dire quel que soit le head de l'expression:
Map[f, Times[a, b, c]]
f[a] f[b] f[c]
D'habitude, on écrit plutôt
Map[f, a * b * c]
f[a] f[b] f[c]
Décomposez l'expression
![]()
en somme de fractions partielles, puis tirez le numérateur de chacun des termes à l'aide des fonctions Map et Apply. On veut le résultat sous la forme d'une liste d'expressions.
Quand on utilise les fonctions Map ou Apply, il faut spécifier une fonction f à appliquer à l'expression. Si cette fonction est déjà définie, ça ne pose pas de problème.
Map[Sin, Table[(n)/6, {n, 0, 11}]]
1 Sqrt[3] Sqrt[3] 1 1 Sqrt[3] Sqrt[3] 1
{0, -, -------, 1, -------, -, 0, --, --------, -1, --------, --}
2 2 2 2 2 2 2 2
Cependant, si l'on veut utiliser une fonction particulière dans une instruction Map, il est nécessaire de définir une fonction particulière uniquement dans ce but.
h[x_] := 2 x2
Map[h, {a, b, c}]
{2 a2, 2 b2, 2 c2}
Clear[h]
Cette façon de faire est maladroite, puisque l'on n'a généralement pas besoin de la fonction ailleurs que dans le Map. Nous allons maintenant voir comment éviter cette définition de fonction inutile.
On observe que lorsqu'on utilise la fonction h dans Map, son nom n'a aucune importance. On a seulement besoin d'un objet qui calcule le double du carré d'une expression quelconque. Il suffirait alors de donner cet objet, plutôt que h, à Map.
L'expression Function[x, 2 x2] joue précisément ce rôle très important dans Mathematica. On l'appelle une fonction pure. Le premier paramètre de Function est la «variable», tandis que le deuxième est le résultat de la fonction appliquée à cette «variable». On remarque en fait que c'est exactement ainsi que l'on procède en mathématiques pour définir une fonction. On observe d'ailleurs que l'expression Function[x, 2 x2] est exactement une «fonction» au sens mathématique du terme. On noterait une telle fonction x
2 x2.
Un tel objet peut être appliqué à n'importe quelle expression a, donnant 2 a2.
Function[x, 2 x2][a]
2 a2
Il est bien évident que le nom de la variable ne joue pas de rôle. Il est donc possible de lui donner le nom que l'on veut.
Function[b, 2 b2][a]
2 a2
On peut ainsi utiliser directement ce type d'objet dans une instruction Map ou Apply.
Map[Function[y, 2 y2], {a, b, c}]
{2 a2, 2 b2, 2 c2}
Cette fonction permet de spécifier n'importe quelle opération. Par exemple, Function[z, 0] (la fonction identiquement nulle) et Function[t, Sin[t] + Cos[t]] sont des fonctions pures correctes.
Function[t, Sin[t] + Cos[t]][/4]
Sqrt[2]
On peut aussi fabriquer à l'aide de Function des fonctions pures de plusieurs variables:
Function[{x, y}, x2 + y2][3, 2]
13
Cette construction est pratique, mais lourde, et la spécification du nom des variables est inutile. Il existe des formes plus courtes pour les fonctions pures, qui utilisent le symbole # (dièse). Lorsque Mathematica rencontre un #, il considère que ce caractère représente la variable. Il n'est donc plus nécessaire d'en spécifier le nom comme premier argument. Ainsi Function[Sin[#] + Cos[#]] est équivalent à Function[t, Sin[t] + Cos[t]].
Function[Sin[#] + Cos[#]][/4]
Sqrt[2]
Il existe une forme encore abrégée, qui évite de taper à chaque fois le mot Function. La forme expression& est équivalente à la forme Function[expression]. Par conséquent on peut récrire la dernière ligne:
(Sin[#] + Cos[#]) &[/4]
Sqrt[2]
Lorsque l'on veut une fonction de plusieurs arguments, les différentes variables se noteront #1, #2, etc.
Function[#12 + #22][a, b]
a2 + b2
ou bien
(#12 + #22 &)[a, b]
a2 + b2
Ecrivez une fonction qui arrondit au dixième une liste de nombres. Généralisez ensuite votre fonction pour n'importe quel arrondi. Essayez d'utiliser les notations abrégées des fonctions pures.
Indication: pour arrondir un nombre x au dixième, on utilise la fonction f suivante:
![]()
Nous avons déjà appris dans le chapitre II quelques notions sur les graphiques Mathematica. Nous allons maintenant nous intéresser à la fabrication de dessins plus complexes. À cette fin, nous avons besoin de comprendre de façon plus complète la structure des graphiques.
Nous avons besoin de connaître la forme InputForm des graphiques. On se souvient qu'il s'agit d'une forme qu'il est possible de saisir intégralement avec un clavier standard (voir le début du chapitre II). Nous allons utiliser cette forme pour comprendre la structure d'un dessin fabriqué par Mathematica. Voici un dessin.
Plot[Floor[x], {x, -1, 2}]

- Graphics -
Et voici sa structure interne
InputForm[%]
Graphics[{{Line[{{-0.9999998749999999, -1.},
{-0.8782990252812526, -1.}, {-0.7455736004218788, -1.},
{-0.6209219009843841, -1.}, {-0.5010447766875156, -1.},
{-0.3734422275312735, -1.}, {-0.2506142535156577, -1.},
{-0.1880127894183947, -1.}, {-0.1200608546406682, -1.},
{-0.08831508843652922, -1.}, {-0.05466097959711609, -1.},
{-0.02592898989597323, -1.}, {-0.01805713048399118, -1.},
{-0.009589288495406655, -1.}, {-0.005491224239854847, -1.},
{-0.001595630472557907, -1.}, {0.001901113341115486, 0.},
{0.005717969093694913, 0.}, {0.1337510414217949, 0.},
{0.2570095386092686, 0.}, {0.387993460656116, 0.},
{0.5142028075623371, 0.}, {0.6356375793279319, 0.},
{0.7647977759529004, 0.}, {0.8291266538089788, 0.},
{0.8891833974372426, 0.}, {0.9469105342036773, 0.},
{0.9635333049839262, 0.}, {0.9792804380416999, 0.},
{0.9861349215485281, 0.}, {0.9934169485272414, 0.},
{0.9972136328890666, 0.}, {0.9993751651011081, 0.},
{1.001404437691494, 1.}, {1.004960734681106, 1.},
{1.008794443780958, 1.}, {1.138385163577785, 1.},
{1.263201308233985, 1.}, {1.383242877749558, 1.},
{1.511009872124506, 1.}, {1.634002291358827, 1.},
{1.764720135452522, 1.}, {1.89066340440559, 1.},
{1.999999875, 1.}}]}},
{PlotRange -> Automatic, AspectRatio -> GoldenRatio^(-1),
DisplayFunction :> $DisplayFunction, ColorOutput -> Automatic,
Axes -> Automatic, AxesOrigin -> Automatic, PlotLabel -> None,
AxesLabel -> None, Ticks -> Automatic, GridLines -> None,
Prolog -> {}, Epilog -> {}, AxesStyle -> Automatic,
Background -> Automatic, DefaultColor -> Automatic,
DefaultFont :> $DefaultFont, RotateLabel -> True, Frame -> False,
FrameStyle -> Automatic, FrameTicks -> Automatic,
FrameLabel -> None, PlotRegion -> Automatic,
ImageSize -> Automatic, TextStyle :> $TextStyle,
FormatType :> $FormatType}]
La partie intéressante est constituée par la fonction Line, dont l'argument est une liste de points, c'est-à-dire de listes de deux nombres (les coordonnées). Les différentes options utilisées sont aussi données.
Mathematica représente chaque dessin comme une expression dont le head est Graphics, c'est-à-dire une structure du type Graphics[a, b, ...], où a, b, ... sont des primitives et des directives graphiques.
Extrayez en une seule commande la liste des points du graphique précédent.
Pour les graphiques en trois dimensions, rien ne change, sauf le head qui est alors Graphics3D. Les primitives graphiques ont alors comme argument des listes de trois coodonnées.
Voici les primitives graphiques les plus employées pour les dessins dans le plan: Point, Line, Polygon, Circle, Disk, Text. Les arguments de ces primitives sont les coordonnées des points constituant les figures à dessiner.
À l'aide de ces commandes, nous pouvons fabriquer «à la main» une expression graphique:
Graphics[{
Point[{0, 3/4}], Line[{{-1, -1}, {0, 1}, {1, -1}}],
Polygon[{{1/2, .8}, {3/4, 1}, {.9, .35}}],
Circle[{0, .2}, 1/2], Disk[{0, -.75}, 1/4],
Text["Voici un beau dessin", {0, -.25}]}]
- Graphics -
Nous avons déjà vu la commande qui permet d'afficher une expression graphique, c'est Show. Pour avoir tout de suite les bonnes proportions, on ajoute l'option AspectRatio -> Automatic.
Show[%, AspectRatio -> Automatic]

- Graphics -
Ce petit exemple montre de façon assez explicite la syntaxe des primitives graphiques de base. Les coordonnées peuvent être données comme dans l'exemple ci-dessus en nombres exacts ou en approximation. Cependant, on gagnera de l'efficacité en utilisant les approximations. En effet, une précision infinie n'est ici pas primordiale.
On peut ajouter les options que l'on veut, par exemple ajouter des axes et un titre:
Show[%, Axes -> Automatic, PlotLabel -> Dessin]

- Graphics -
Dessinez un pentagone régulier à l'aide de la primitive graphique Line, puis à l'aide de Polygon.
Faites sur ce modèle un programme polygone[n] qui dessine un polygone régulier à n côtés.
Pour les dessins tridimensionnels, la syntaxe est analogue. La primitive Cuboid permet de dessiner un cube en donnant les coordonnées de deux sommets opposés. On aura par exemple:
Show[
Graphics3D[{
Line[{{-1, -1, -1}, {.3, -.75, -.5}, {-.8, .3, .35}, {1, 1, 1}}],
Cuboid[{1, -1, -1}, {.5, -.5, -.5}], Point[{-.25, -.5, -.25}],
Text["Texte", {-.8, -.8, 0}],
Polygon[{{1/2, -.8, 0}, {-3/4, 1, .4}, {.9, .35, .2}}]}
],
Axes -> Automatic, AxesLabel -> {"x", "y", "z"}]

- Graphics3D -
Pour voir ce dessin sous un autre angle, on utilise l'option ViewPoint, qui donne les coordonnées du point de vue, relativement au centre de la boîte contenant le graphique, le plus long côté de la boîte étant pris pour unité. On peut choisir ce point de vue interactivement avec la commande 3D ViewPoint Selector... du menu Input.
Show[%, ViewPoint -> {2.7, -2, 0.25} ]

- Graphics3D -
Show[%, ViewPoint -> {-1.7, 0.8, 3}]

- Graphics3D -
Nous savons maintenant fabriquer des graphiques plus compliqués, mais d'aspect uniforme. Il est bon de changer parfois l'épaisseur d'un trait, d'introduire des traitillés, de changer la couleur d'un élément graphique, de modifier la grandeur d'un point. Mathematica permet d'effectuer ces tâches à l'aide des directives graphiques comme Thickness, Dashing, RGBColor, GrayLevel ou PointSize. On introduit ces directives avant le ou les éléments graphiques qui doivent être modifiés par elles.
En groupant dans une liste des directives graphiques avec des primitives graphiques, on «localise» l'effet des directives. L'exemple ci-dessous montre que la directive GrayLevel[.5] ne s'applique qu'au disque qui se trouve dans la même liste. Le texte qui suit («Voici un dessin en couleurs») est en effet toujours rouge.
À moins que l'on ne la change explicitement, une directive graphique garde son effet pour tous les éléments suivants du dessin ou de la liste, le cas échéant.
Show[
Graphics[{
PointSize[.04], Point[{0, 3/4}],
Thickness[.015], Line[{{-1, -1}, {0, 1}, {1, -1}}],
RGBColor[1, 0, 0], Polygon[{{1/2, .8}, {3/4, 1}, {.9, .35}}],
Dashing[{.02, .05}], Thickness[.01], Circle[{0, .2}, 1/2],
{GrayLevel[.5], Disk[{0, -.75}, 1/4]},
Text["Voici un dessin en couleurs", {0, -.25}]}
], AspectRatio -> Automatic]

- Graphics -
Le ou les arguments des directives graphiques PointSize, Thickness et Dashing sont les dimensions relatives, données en fraction de la largeur du dessin. C'est très pratique, puisque les proportions restent les mêmes lorsqu'on redimensionne un graphique. Il existe cependant des cas où l'on veut spécifier une valeur fixe pour une épaisseur, etc. On utilisera alors les variantes AbsolutePointSize, AbsoluteThickness et AbsoluteDashing. L'unité du paramètre est ici le point d'imprimante, qui vaut approximativement 0.35 mm (il y a environ 72 points dans un pouce).
GrayLevel donne un niveau de gris entre 0 (noir) et 1 (blanc), tandis que RGBColor donne une couleur à partir de ses composantes rouge, verte et bleue, chacune entre 0 et 1 (RGB signifie red, green, blue). On peut aussi spécifier une couleur à l'aide de Hue[t, s, l], où t est la teinte, s la saturation et l la luminosité, toutes entre 0 et 1 également.
Les directives graphiques s'utilisent de la même façon en dessin tridimensionnel.
Pour connaître la liste exhaustive de tous les éléments et de toutes les directives graphiques, ainsi que des précisions concernant leur emploi, on consultera l'aide ou le manuel de référence de Mathematica.
Dessinez un polygone quelconque, avec la surface en bleu et le contour d'une bonne épaisseur en orange.
Indication: l'orange est composé de rouge à 100% et de vert à 50% (pas de bleu).
Dessinez un cube avec quelques-uns de ses axes de symétrie (d'une bonne épaisseur, traitillés et en couleur).
Lorsque l'on effectue des calculs ou d'autres opérations avec Mathematica, on ressent souvent le besoin de se référer à des résultats antérieurs, ou à d'autres fonctions. Nous avons déjà rencontré plusieurs mécanismes permettant d'utiliser des résultats précédents.
Une technique plus rationnelle et plus efficace consiste à regrouper une suite d'instructions. Il est possible de faire cela en utilisant comme en Pascal des procédures. En Mathematica, une procédure est simplement une suite d'instructions (ou commandes) séparées par des points-virgules. Les expressions sont évaluées dans l'ordre les unes après les autres et le résultat de la procédure est celui de la dernière instruction.
r = (1 + x)2; r = Expand[r]; r = r - 1
2 x + x2
r =.
Bien sûr comme cela, ça ne sert pas à grand-chose. Cependant on peut définir une fonction qui exécutera ces pas successifs sur son argument. À noter l'utilisation de parenthèses pour grouper les instructions successives.
f[x_] := (t = (1 + x)2; t = Expand[t]; t = t - 1)
Après l'évaluation de cette ligne, f est la fonction ou plutôt le programme qui exécute nos instructions.
f[a + b]
2 a + a2 + 2 b + 2 a b + b2
Malheureusement, un effet de bord de notre procédure est d'affecter une valeur à la variable t, ce qui n'est pas nécessaire, ni souhaitable. En effet t joue ici le rôle de variable temporaire.
t
2 a + a2 + 2 b + 2 a b + b2
Nous aimerions garder locales les variables temporaires, de sorte qu'après exécution de la procédure, aucune valeur ne leur soit affectée. D'autre part, il est absolument nécessaire que des variables temporaires de deux programmes distincts ne provoquent pas de conflits et soient indépendantes, même si par coïncidence elles ont le même nom.
t =.
Pour protéger les variables, Mathematica offre les constructions Block[{x, y, ...}, procédure] et Module[{x, y, ...}, procédure]. La liste des variables à traiter localement par la procédure est spécifiée comme premier argument de cette fonction.
Avec Block et Module, chaque fois que la procédure est exécutée, la valeur originale de chaque variable (s'il y en a une) est sauvegardée et restaurée à la fin de l'exécution.
g[x_] := Block[{u}, u = (1 + x)2; u = Expand[u]; u = u - 1]
g[a + b]
2 a + a2 + 2 b + 2 a b + b2
Il est facile de vérifier que la variable u n'a pas reçu de valeur globale:
u
u
On peut ainsi imbriquer des procédures de façon sûre, et utiliser Mathematica comme un langage de programmation structurée (comme Pascal ou Ada).
La construction Module ou Block permet également de spécifier des valeurs initiales pour les variables à garder localement. Ainsi on pourrait écrire:
g[x_] := Block[{u = (1 + x)2}, u = Expand[u]; u = u - 1]
g[a + b]
2 a + a2 + 2 b + 2 a b + b2
N'oublions pas d'ôter les définitions de fonctions qui ne sont pas nécessaires pour la suite:
Clear[f, g]
Du point de vue interne à Mathematica, il y a une subtile différence entre Block et Module. Le premier des deux traite localement les valeurs des variables seulement. Quant à Module, il traite localement les noms et valeurs des variables. Dans les langages de programmation classiques (Pascal, C, etc.), une procédure agit de la même manière que Module.
Cette subtilité entre Block et Module n'intervient en fait que dans des programmes où l'on désire un fonctionnement particulier. Dans la plupart des cas, le fonctionnement est identique. Voici une situation où le comportement des deux fonctions est différent:
m = i2
i2
Block[{i = a}, i + m]
a + a2
Module[{i = a}, i + m]
a + i2
Clear[m]
Du point de vue de l'efficacité, Block est au moins 3 fois plus rapide que Module. Si l'on ne modifie pas la valeur des variables à l'extérieur d'un Block, le fonctionnement est parfaitement identique.
Comme tout langage de programmation, Mathematica comporte un certain nombre de structures de contrôle, qui permettent de diriger le «flux» du programme.
Tout d'abord Mathematica permet de faire des boucles, c'est-à-dire de répéter un certain nombre de fois une séquence d'instructions. Mathematica possède plusieurs sortes de constructions pour faire des boucles.
L'instruction Do est celle qui ressemble le plus aux langages de programmation structurée. Cette commande utilise très simplement un itérateur, comme les fonctions Table ou Integrate. Le premier argument est la procédure à effectuer à chaque itération. L'exemple suivant montre comment afficher à l'écran les carrés des cinq premiers nombres naturels. On remarque que Do ressemble beaucoup au FOR du langage Pascal.
Do[Print[i2], {i, 5}]
1
4
9
16
25
Comme la notion d'itérateur est très souple, on peut faire des boucles particulières. Ici la variable d'itération diminue de 6 à 0 par pas de 2:
Do[Print[i2], {i, 6, 0, -2}]
36
16
4
0
Une instruction Do peut être mise dans un Block ou un Module. La boucle de l'exemple suivant ne comporte pas de variable d'itération. L'itérateur, réduit à sa plus simple expression {5}, indique que la suite d'instructions doit être effectuée 5 fois. On remarque l'initialisation de la variable locale dans le Block.
Block[{n = 0}, Do[n = n + 1; Print[n], {5}]]
1
2
3
4
5
Il est possible d'effectuer des boucles imbriquées à l'aide d'un seul Do, en donnant une suite d'itérateurs.
Do[Print[{i, j}], {i, 4}, {j, i - 1}]
{2, 1}
{3, 1}
{3, 2}
{4, 1}
{4, 2}
{4, 3}
Cet exemple montre qu'une variable d'itération peut dépendre d'une autre. Attention dans ce cas à l'ordre des itérateurs. L'expression suivante ne convient pas:
Do[Print[{i, j}], {j, i - 1}, {i, 4}]
Do[Print[{i, j}], {j, i - 1}, {i, 4}]
Voici une situation plus compliquée où l'on peut déjà observer la puissance de notre environnement de programmation.
Module[{t = x}, Do[t = 1/(1 + t), {3}]; t]
Ecrivez à l'aide de Block (ou Module) et Do une procédure qui calcule n factorielle.
Mathematica possède aussi l'équivalent du WHILE de Pascal. Cette fonction s'appelle évidemment While. Son premier argument est un test, ou ce qui revient au même une variable booléenne. Le deuxième argument est la procédure à exécuter, tant que le test donne la valeur True.
Block[{n = 0}, While[n < 5, n = n + 1; Print[n]]]
1
2
3
4
5
Ecrivez à l'aide de Block (ou Module) et While une procédure qui exécute l'algorithme d'Euclide pour calculer le pgcd de deux nombres entiers.
Mathematica connaît encore d'autres sortes de boucles, qui peuvent être très utiles et sont en général plus efficaces que les précédentes: Nest, Fold et FixedPoint. Nest est en fait la traduction Mathematica du concept de la composition multiple d'une même fonction:
Nest[f, x, 4]
f[f[f[f[x]]]]
Par exemple,
Nest[Sqrt, x, 3]
x1/8
Cette fonction est pratique pour exécuter de façon répétée la même opération. On peut évidemment la combiner avec des fonctions pures:
Nest[1/(1 + #) &, x, 3]
Si l'on a besoin des résultats obtenus lors de chaque itération, on utilisera la fonction NestList qui fonctionne exactement de la même manière, donnant une liste des résultats intermédiaires.
NestList[1/(1 + #) &, x, 3]
On sait que la suite (a/n + n)/2 converge vers la racine carrée de a. Développez une fonction Mathematica qui calcule une approximation de la racine carrée de a à l'aide cette suite et de la fonction Nest.
La fonction FixedPoint est très proche de Nest. Si celle-ci compose une fonction un nombre déterminé de fois, celle-là applique la fonction jusqu'à ce que le résultat ne change plus. En fait, la fonction FixedPoint est très précieuse, car elle permet d'imiter la façon dont Mathematica travaille (principe du point fixe).
À l'aide de FixedPoint, on peut par exemple résoudre l'exercice précédent de manière extrêmement efficace:
racine[a_, x0_] := FixedPoint[1/2 (# + a/#) &, x0]
racine[5, N[1, 25]]
2.236067977499789696409174
Clear[racine]
Là encore, il existe la version qui retourne toute la liste:
FixedPointList[1/2 (# + 5/#) &, N[1, 25]]
{1.000000000000000000000000, 3.000000000000000000000000,
2.333333333333333333333333, 2.238095238095238095238095,
2.236068895643363728470111, 2.236067977499978194094594,
2.236067977499789696409174, 2.236067977499789696409174}
Ecrivez un programme qui calcule un zéro d'une fonction à l'aide de la méthode de Newton. Utilisez votre programme pour calculer&pgr; avec 40 décimales correctes.
La dernière des boucles spécifiques de Mathematica est Fold.
Pour bien comprendre à quoi sert la fonction Fold, il est utile de regarder comment fonctionne la version liste FoldList:
FoldList[f, x, {a, b, c}]
{x, f[x, a], f[f[x, a], b], f[f[f[x, a], b], c]}
On observe que FoldList applique la fonction de deux variables f sur des paires successives. Au i-ème pas, le premier élément de la paire est le résultat obtenu à l'itération précédente, tandis que le deuxième est l'élément i de la liste. Le résultat du pas 0 est le deuxième argument (dans cet exemple x). Si par exemple la fonction est Plus, on aura:
FoldList[Plus, x, {a, b, c}]
{x, a + x, a + b + x, a + b + c + x}
La fonction Fold fonctionne de la même manière, mais ne donne que le dernier élément de la liste:
Fold[Plus, x, {a, b, c}]
a + b + c + x
Fabriquez à l'aide de Fold un programme qui transforme une liste de chiffres en un nombre dont les chiffres sont ceux de la liste, par exemple qui donne 123 si on lui donne en input la liste {1, 2, 3}.
Indication: il est pratique de fabriquer d'abord une fonction qui multiplie son premier argument par 10 et lui ajoute le deuxième, donnant par exemple {12, 7}
127.
Mathematica possède aussi, comme tout langage de programmation, des conditionnelles. La plus utilisée est bien entendu If. Sa syntaxe est standard: If[test, alors, sinon] exécute alors si test donne la valeur True, et sinon si test donne False.
Par exemple la ligne suivante définit la valeur absolue.
abs[x_] := If[x >= 0, x, -x]
Plot[abs[x], {x, -2, 2}]

- Graphics -
Il existe une autre manière de définir des fonctions par morceaux. Cette méthode utilise la notation /; (barre oblique et point-virgule sans espace) qui désigne une condition (on lit cet opérateur «si» ou «quand», ou encore «pourvu que»).
g[x_] := x /; x >= 0
signifie qu'il faut utiliser la règle g[x_] := x seulement si le paramètre x est positif ou nul. On complète alors la définition de g par:
g[x_] := -x /; x < 0
Plot[g[x], {x, -2, 2}]

- Graphics -
Pour la définition de fonctions par morceaux, cette dernière approche est souvent meilleure et permet d'avoir plus de deux morceaux.
Clear[abs, g]
Définissez à l'aide de la fonction If une fonction qui retourne la valeur True si l'entier n divise l'entier m et False sinon.
Définissez à l'aide de l'opérateur /; la fonction signum, c'est-à-dire
![]()
Quand on conçoit et met en oeuvre des programmes pour un utilisateur autre que soi-même, il est absolument nécessaire de documenter ses programmes.
Il existe deux façons de faire cela avec Mathematica. La première consiste à introduire dans le texte du programme des commentaires qui permettront à l'utilisateur de comprendre son fonctionnement et ses éventuelles subtilités. On place les commentaires entre (* et *). Le texte compris entre ces deux symboles est ignoré par Mathematica lors de l'exécution des commandes.
Une autre façon de procéder, surtout si l'on fabrique des packages, est d'assigner à chaque fonction définie un texte descriptif, à l'aide de la commande fonction::usage = "texte". Ce court texte expliquera comment utiliser la commande et quel sera son résultat. L'utilisateur peut obtenir ce texte avec ?, comme s'il s'agit d'une commande de Mathematica.
Voici un exemple où l'on utilise les deux méthodes de documentation.
carre[x_] := x2 (* le carré de x *)
carre :: usage = "carre[x] calcule le carré de x."
carre[x] calcule le carré de x.
?carre
carre[x] calcule le carré de x.
??carre
carre[x] calcule le carré de x.
carre[x_] := x^2
On imite ainsi la structure interne de Mathematica.
Pour enlever de la mémoire les messages assignés à une fonction, la fonction Clear ne suffit pas. On utilise alors la fonction ClearAll.
Clear[carre]
?carre
carre[x] calcule le carré de x.
ClearAll[carre]
??carre
Global`carre
On a remarqué que Mathematica est un environnement de programmation interprété, et non compilé: les instructions ou programmes sont effectués séquentiellement, sans transformation du code en langage machine.
Il est cependant possible de compiler des programmes, afin d'accélérer leur exécution, avec la commande Compile. L'utilisation de cette commande est semblable à celle de Function. Sa syntaxe est la suivante: Compile[{x1, x2, ...}, expression], où les xi sont les variables de la fonction et expression sa définition.
Voyons son fonctionnement à l'aide d'un exemple, la fonction x
x sin(x). À l'aide de Function, on définit la fonction de manière habituelle, c'est-à-dire sans compilation.
f = Function[{x}, x Sin[x]]
Function[{x}, x Sin[x]]
Voici la même fonction, mais cette fois compilée:
fc = Compile[{x}, x Sin[x]]
CompiledFunction[{x}, x Sin[x], -CompiledCode-]
On observe que les deux fonctions ont le même comportement sur un nombre réel:
{f[3.45], fc[3.45]}
{-1.04722, -1.04722}
En compilant cette fonction, Mathematica traite automatiquement le paramètre comme un réel. Cela lui évite de perdre du temps en choisissant l'algorithme approprié, suivant que l'argument est un entier, une liste, une expression algébrique, etc.
Clear[f, fc]
Il est possible de spécifier le type des arguments de la fonction compilée. L'input ci-dessous montre comment indiquer que les paramètres sont des entiers (précision infinie). On remarque le signe _ (souligné), que l'on a déjà rencontré lors de la définition de fonctions. Ici, _Integer signifie «n'importe quelle expression de type (head) Integer». Si aucun type n'est spécifié, l'argument est considéré comme réel.
f = Compile[{{i, _Integer}, {j, _Integer}}, 10 i + j]
CompiledFunction[{i, j}, 10 i + j, -CompiledCode-]
f[7, 5]
75
Si le type des arguments ne correspond pas à la définition de la fonction, Mathematica en avertit l'utilisateur et utilise la fonction non compilée pour calculer le résultat.
f[4.5, 5]
50.
L'intérêt de la compilation n'est jusqu'ici pas évident. En revanche, si la procédure doit être effectuée un grand nombre de fois successivement, la compilation accélère considérablement le calcul. L'exemple ci-dessous montre une telle accélération. On implémente la méthode d'approximation des racines carrées (Newton).
Voici la fonction non compilée:
f = Function[{x, n}, Block[{t = x}, Do[t = (t + x/t)/2, {n}]; t]];
et sa version compilée:
fc = Compile[{x, {n, _Integer}},
Block[{t = x}, Do[t = (t + x/t)/2, {n}]; t]];
La fonction Timing permet de connaître le temps mis par le processeur pour trouver exécuter les instructions. Pour 5000 itérations, on a:
Timing[f[2., 5000]]
{1.15 Second, 1.41421}
Timing[fc[2., 5000]]
{0.0666667 Second, 1.41421}
La version compilée est plus de 10 fois plus rapide que la version standard.
Clear[f, fc]
Depuis la version 3.0 de Mathematica, il est possible de compiler des expressions contenant des listes. Dans ce cas, il faut donner en plus du type des éléments de la liste (qui doit être le même pour tous les éléments), la dimension de la liste, c'est-à-dire 1 pour une liste simple, 2 pour une matrice, etc.
Le programme suivant prend deux listes simples (vecteurs) comme arguments et calcule leur somme composante par composante.
s = Compile[{{m, _Real, 1}, {n, _Real, 1}}, m + n];
s[{1, 2, 3}, {1, 3, 5}]
{2., 5., 8.}
s[{1, 2, 3, 4, 5}, {2, 3, 4, 5, 6}]
{3., 5., 7., 9., 11.}
Le programme suivant prend une matrice à coefficients entiers comme argument et calcule la somme de ses éléments.
t = Compile[{{m, _Integer, 2}}, Apply[Plus, Flatten[m]]];
21
38
Clear[s, t]
Écrivez à l'aide de Compile un programme qui multiplie les éléments d'une liste de nombre entiers.
Comme on a déjà pu le voir, il existe différentes façons de programmer en Mathematica. Toutes ces méthodes sont essentiellement équivalentes (thèse de Church). Cependant l'une d'entre elles sera beaucoup plus efficace dans un problème déterminé, ou encore sera plus facile à programmer.
Une analyse du problème est donc nécessaire afin de faire un choix judicieux de la technique à utiliser.
Considérons le programme suivant qui calcule la factorielle d'un entier (solution d'un exercice antérieur).
factorielle[n_] :=
Module[{result = 1}, Do[result = result x i, {i, n}]; result]
factorielle[8]
40320
C'est un exemple de programme procédural, type Pascal ou Ada. La programmation procédurale est le type de programmation de plus bas niveau que Mathematica permette. En programmant de cette manière, il faut spécifier tous les détails tels que variables temporaires, initialisations, etc. En utilisant des formes plus évoluées de programmation, on peut se concentrer sur le problème lui-même, sans se préoccuper de ce genre de détails.
Mathematica permet de programmer de façon beaucoup plus élégante. Il suffit pour s'en convaincre d'observer les deux programmes suivants -- le premier en style procédural, le second en style fonctionnel -- qui font exactement la même chose.
Module[{t = x}, Do[t = 1/(1 + t), {3}]; t]
Nest[1/(1 + #) &, x, 3]
Pour toutes les raisons évoquées, on évitera dans la mesure du possible de programmer en style procédural.
La programmation récursive est déjà une forme plus évoluée de programmation Mathematica. Regardons l'exemple suivant.
Le programme factorielle peut être grandement amélioré en utilisant la récursion:
factorielle[0] = 1; factorielle[n_Integer] := n factorielle[n - 1];
factorielle[12]
479001600
La récursion est souvent une manière très naturelle de concevoir la solution d'un problème. Il est cependant des cas où la récursion n'est pas idéale. Revenons pour illustrer cela aux nombres de Fibonacci.
Si l'on utilise la fonction que l'on a définie au chapitre premier:
fib[0] = fib[1] = 1; fib[n_Integer] := fib[n - 1] + fib[n - 2]
on aura vite des problèmes. En effet en calculant par exemple le vingtième nombre de Fibonacci, on aura à calculer un grand nombre de fois le huitième par exemple. Il est facile d'illustrer cela.
Nous écrivons la même définition en ajoutant une partie qui affiche à l'écran le nombre de Fibonacci actuellement calculé.
Clear[fib]
fib[0] = fib[1] = 1; fib[n_Integer] := (Print[n]; fib[n - 1] + fib[n - 2]);
Voyons maintenant ce qui se passe en calculant le fib[6]:
fib[6]
6
5
4
3
2
2
3
2
4
3
2
2
13
Clear[fib]
On remarque que par exemple fib[3] a été calculé 3 fois et fib[2] 5 fois. Nous avons vu comment éviter cela au chapitre 2. On définit une fonction qui stocke les valeurs déjà calculées:
fib[0] = fib[1] = 1; fib[n_Integer] := (Print[n]; fib[n] = fib[n - 1] + fib[n - 2]);
fib[6]
6
5
4
3
2
13
Cette fois chaque valeur n'est calculée qu'une seule fois. Cela augmente naturellement la performance du programme, mais en contrepartie la mémoire stocke les résultats intermédiaires. Le kernel a en effet retenu toutes les valeurs de la fonction déjà calculées:
?fib
Global`fib
fib[0] = 1 fib[1] = 1 fib[2] = 2 fib[3] = 3 fib[4] = 5 fib[5] = 8 fib[6] = 13 fib[n_Integer] := (Print[n]; fib[n] = fib[n - 1] + fib[n - 2])
Le calcul du n-ième nombre de Fibonacci devient rapidement prohibitif en terme de mémoire, pour n suffisamment grand.
Clear[fib]
La programmation fonctionnelle est un type de programmation particulièrement adéquat avec Mathematica. Chaque programme consiste dans ce cas en un certain nombre de fonctions imbriquées - «composées» à la manière des mathématiques - chacune d'entre elles agissant sur le résultat de la précédente. Cette technique permet de construire facilement un programme pas à pas.
Voyons à titre d'exemple comment calculer le polynôme caractéristique d'une matrice carrée.
polCaract[m_] := Det[m - x IdentityMatrix[Length[m]]]
Il est intéressant d'ajouter une condition interdisant l'application de cette fonction à des inputs n'étant pas des matrices carrées, pour le genre de comportement suivant:
polCaract[{{1, 2}, {2, 3}, {3, 4}}]
Det[{{1, 2} + {-x, 0, 0}, {2, 3} + {0, -x, 0}, {3, 4} + {0, 0, -x}}]
Clear[polCaract]
polCaract[m_] := Det[m - x IdentityMatrix[Length[m]]] /; MatrixQ[m] && Length[m] == Length[Transpose[m]]
polCaract[{{1, 2}, {2, 3}, {3, 4}}]
polCaract[{{1, 2}, {2, 3}, {3, 4}}]
A = {{2, 1, 3}, {0, 3, 5}, {1, 1, 4}};
polCaract[A]
10 - 18 x + 9 x2 - x3
Il est bien sûr conseillé d'utiliser intensivement les fonctions pures en programmation fonctionnelle. C'est en effet là que leur utilité et leur efficacité sont maximales.
Clear[polCaract, A]
Ecrivez un programme factorielle de style fonctionnel.
Indication: produisez d'abord la liste des nombres à multiplier.
Pour terminer ce chapitre, voici quelques conseils pour rendre les programmes Mathematica plus performants.
On observe avant tout que Mathematica n'est d'une manière générale pas très rapide, puisque c'est un langage interprété, et non compilé. Cependant sa grande force réside dans le nombre très important de programmes déjà à disposition, dont certains sont très rapides. En effet les fonctions Mathematica utilisent les meilleurs algorithmes dans l'état actuel de la recherche.
Un énorme gain de temps a lieu lors du développement du programme. En effet les techniques de programmation ainsi que les notations de Mathematica sont très proches du langage mathématique courant.
La compilation des fonctions et programmes permet en outre d'augmenter sensiblement leur performance.
D'une manière globale, le premier conseil à donner est d'éviter tant que possible la programmation structurée et la récursion, donc d'employer la programmation fonctionnelle le plus souvent que l'on peut.
Si l'on doit faire une procédure, il est préférable d'utiliser Block plutôt que Module.
Avec un petit peu d'expérience, on s'aperçoit que les boucles classiques, c'est-à-dire Do et While, ne sont pas très performantes. On leur préférera donc les structures plus spécifiques, telles que Nest, Fold ou FixedPoint ou leur version liste.
Les listes sont très employées pour programmer en Mathematica, car elles sont performantes. Deux constructions sont très efficaces et donc à conseiller vigoureusement: Range et Table. On aura remarqué que bon nombre de solutions aux exercices les utilisent.
En revanche, toute manipulation de liste est désavantageuse. En effet Mathematica réarrange complètement une liste chaque fois que l'on modifie ne serait-ce qu'un élément de celle-ci. C'est pourquoi il faut aussi éviter dans la mesure du possible les fonctions Append et AppendTo ainsi que Prepend et PrependTo, ainsi que tous les remplacements (avec ou sans la forme abrégée /.), dès que le nombre d'éléments d'une liste dépasse 1000.
Pour construire une liste, on préférera une boucle du type suivant, qui est environ 15 fois plus rapide.
liste = {};
Timing @ (Do[liste = {Random[Integer], liste}, {5000}];)
Timing @ (Flatten[liste];)
{0.383333 Second, Null}
{0.0833333 Second, Null}
En conclusion, on dira que les programmes construits autour de Table, Range, Nest (NestList), Fold (FoldList) et/ou FixedPoint (FixedPointList) sont généralement beaucoup plus performants que tous les autres.
|
Chapitre précédent |
Table des matières |
Chapitre suivant |