SCILAB 2e PARTIE

1












LES FONCTIONS


Dans un programme nous avons très souvent un bloc d'instructions qui effectue un calcul particulier et qui est utilisé à plusieurs endroits avec, éventuellement, des modifications concernant la valeur de ses paramètres. Il serait très fastidieux de écrire plusieurs fois le même code. Nous pouvons à la place utiliser le concept de fonction qui  est justement associée à un ensemble d'instructions considérées, lors de l'exécution, comme un tout.
Une fonction est déterminée par son nom et c'est par ce nom qu'elle sera appelée chaque fois qu'on voudra l'exécuter.
Scilab, comme d'ailleurs tous les autres langages de programmation, possède un très grand nombre de fonctions. Ces fonctions sont automatiquement chargées lors du lancement d'une session.
LES ARGUMENTS D'UNE FONCTION


Très peu de langages (Pascal de façon assez laxiste, Ada de façon très rigoureuse) font la distinction entre les paramètres dont les valeurs seront utilisés comme des entrées pour pouvoir effectuer les calculs et ceux dont on cherche à recupérer les résultats de ces calculs et qui sont, par conséquent, utilisés comme des sorties.
Les paramètres aussi bien d'entrée ou de sortie sont appelés arguments de la fonction. La forme générale de la définition d'une fonction est
function[s1, ..., sN] = nomFonction(e1, ..., eM)
< corps de la fonction >
[endfunction]1
Les arguments e1, ..., eM sont les arguments d'entrée et les arguments s1, ..., sN sont les arguments de sortie d la fonction.
On voit ainsi qu'en Scilab il y a une distinction absolu entre arguments de sortie et arguments d'entrée ce qui augmente la fiabilité du logiciel créé.
Bien sûr un argument peut être en même temps un argument d'entrée et de sortie. Dans ce cas son nom doit figurer à gauche et à droite du signe d'égalité dans la définition de la fonction.
PASSAGE DES ARGUMENTS


L'appel à la fonction précédente se fait à l'intérieur d'un programme, dit programme appelant, à l'aide de l'instruction
[y1, ..., yN] = nomFonction(x1, ..., xM)
On remarque qu'il n'est pas nécessaire d'utiliser les mêmes noms que lors de la définition de la fonction.
Il est intéressant de comprendre la façon dont s'établit la communication entre le programme appelant et la fonction appelée.
Examinons d'abord un argument d'entrée, par exemple xK auquel correspond l'argument eK de la définition de la fonction. Le passage de l'argument entre les deux programmes se fait, comme on dit, par valeur. C'est-à-dire la valeur de xK est stockée dans une autre adresse mémoire à laquelle pointe l'argument eK. De cette façon la fonction recupère la valeur de l'argument mais pas son adresse. Donc si dans la fonction on modifie la valeur de cet argument, cette modification n'affectera pas la variable xK et au retour de l'appel de la fonction, xK aura la valeur qu'elle avait avant l'appel à la fonction. En somme modifier les valeurs des arguments en entrée ne peuvent pas être modifiées par une fonction, sauf à l'intérieur de celle-ci.
En ce qui concerne les arguments de sortie, le passage se fait par adresse. Soit, par exemple, l'argument yL de l'appel, auquel correspond l'argument sL de la définition de la fonction. sL pointe à l'adresse de yL. Ainsi toute modification de la valeur de sL se répercutera aussitôt à l'argument yL dans la mesure qu'une seule et même adresse correspond à ces deux arguments.
Deux erreurs sont à éviter lors de l'écriture du code d'une fonction :
PORTÉE DES VARIABLES


Nous abordons maintenant un problème qui est traité de manière laxiste par tous les langages de programmation impérative, sauf Ada. Il s'agit de la portée des variables, c'est-à-dire de la question suivante : si, à l'intérieur d'une fonction, on définit une variable, est-ce qu'elle visible, sans passage par argument, par d'autres fonctions et lesquelles ? La réponse de Scilab est affirmative mais l'affaire n'est pas simple. Examinons donc en détail la portée des variables en essayant de comprendre son mécanisme.
Soit la fonction f1 dans laquelle on définit une variable nomVar en lui attribuant une valeur quelconque. On suppose que dans le déroulement du programme c'est la première fois où on rencontre le nom de cette variable et par conséquent c'est à ce moment là que Scilab associe nomVar  avec une adresse mémoire, mettons addrNomVar1. Cette variable est appélée variable locale. Elle est visible, en lecture seulement, par toute fonction qui est appelée par f1 après sa définition et aussi par toute fonction appelée par les fonctions appelées par f1. On dit bien en lecture seulement, ce qui signifie que si, dans une fonction f2 appelée par f1, on veut assigner une autre valeur à nomVar, alors Scilab crée une autre variable avec le même nom mais avec une adresse différente, par exemple addrNomVar2. Ainsi quand on retourne à la fonction f1, nomVar est associée à addrNomVar1 et ella ainsi la valeur définie dans f1. Il reste à savoir ce qui se passe si f2 appelle à son tour une autre fonction f3 et celle-ci utilise aussi la variable nomVar. En Scilab, f3 a accès à la variable nomVar de f2, c'est-à-dire à l'adresse addrNomVar2. Elle ne pourra pas accéder à la variable nomVar de l'adresse addrNomVar1.
Pour illustrer ce propos, examinons le programme suivant :

function[res] = mult()
res = x*test;
endfunction
function[res] = cube();
test = x*x; res = mult();
endfunction;
// Programme principal
test = 1; x = 2; y = cube() test

Le résultat de l'exécution du programme est

test = 1.
y = 8.
test = 1.
exec done

Étudions le comportement du programme. Au programme principal, on définit deux variables test et x avec comme valeurs 1 et 2 respectivement. Notons que ces variables sont, pour Scilab, des variables locales bien qu'elles soient visibles par la totalité du programme (ce qui n'est pas le cas avec le C ou le Fortran). On appelle la fonction cube qui doit retourner la valeur de x à la puissance 3. Dans le programme cube, on modifie la valeur de test. Donc Scilab crée une autre variable avec le même nom et une adresse différente pour stocker la nouvelle valeur de test. Ensuite on appelle la fonction mult qui utilise la valeur de test. La valeur qui est utilisée ici est la valeur qui a eu test dans la fonction cube et non pas la valeur donnée par le programme principal, ce qui explique le résultat final y = 8. Au retour au programme principal, test retrouve la valeur qu'il a eu au programme principal, ce qui explique l'affichage test = 1. Notons pour terminer que la valeur assignée à test par la fonction cube est perdue après la fin de cette fonction. C'est la raison pour laquelle on appelle ces variables locales, car leur existence est solidaire de la fonction dans laquelle elles sont définies. Elles sont créées par des fonctions et elles disparaissent dès la fin des celles-ci.


Scilab possède aussi des variables globales. On le déclare par l'intermédiaire de l'instruction global. Ainsi global('x') détermine x comme une variable globale. Cette variable à une portée générale en lecture et en écriture. Il faut seulement que chaque fonction qu'elle l'utilise, fasse référence à celle-ci par l'intermédiare de la même déclaration global. En effet si cette déclaration est absente et la fonction utilise x, Scilab considérera x comme étant une variable locale définie dans cette fonction.
Reprenons le même programme et déclarons la variable test comme globale. On a donc

function[res] = mult()
res = x*test;
endfunction
function[res] = cube();
global('test'); test = x*x; res = mult();
endfunction;
// Programme principal
global('test'); test = 1 x = 2; y = cube() test

Les résultats sont maintenant

test = 1.
y = 8.
test = 4.
exec done

On voit que la valeur de test a été changée, c'est-à-dire que nous avons pu modifier le contenu de l'adresse qui est associée à test.
Cette dernière possibilité fournit la différence qui existe entre variable locale et variable globale. Sans cette dernière possibilité, les deux types de variables seraient identiques.
Les variables globales peuvent aussi permettre le contournement des problèmes de mémoire. Nous avons signaler plus haut que lorsqu'on passe par arguments d'entrée des grands tableaux, il y a risque de dépassement de la capacité de la mémoire car les entrées d'une fonction sont dupliquées. Ce risque peut être évité si on utilise pour ces grands tableaux des variables globales, parce que dans ce cas les variables ne sont pas dupliquées.
En terminant ce paragraphe, il est utile de faire une remarque générale concernant le passage des valeurs des variables entre différentes fonctions. Scilab, comme d'autres langages de programmation, est assez laxiste et permet le passage des variables en entrée sans mention explicite dans la liste des arguments en entrée. Il faut le dire haut et fort : l'utilisation de cette pratique est un de plus grands dangers qui menacent la fiabilité du logiciel. Écrire des fonctions qui utilisent des variables qui n'apparaissent pas comme des arguments en entrée, ne facilite pas la tâche de conception du logiciel, surtout quand ce logiciel est développé par une ou plusieurs équipes. Il ne facilite pas non plus la maintenance du logiciel. Il faut bien savoir que l'industrie du logiciel se trouve toujours confronter à la fiabilité et à la sécurité du logiciel et il n'est pas raisonnable d'alourdir la difficulté de l'écriture du code avec des pratiques contestables. Il est en effet regrettable que les concepteurs de Scilab n'ont pas voulu s'inspirer des techniques de production du logiciel utilisées par Ada mais il n'est pas nécessaire de le suivre dans cette voie. Nous pouvons essayer d'écrire des fonctions de sorte que chaque variable en entrée y figure explicitement dans la liste des arguments en entrée, aussi fastidieuse soit-elle cette pratique. Bien évidemment cette remarque ne concerne pas les variables globales qui, comme nous avons vu, pour être utilisées, doivent être déclarées par la commande global.
PLACE DES FONCTIONS


Une fonction peut être placée :
La bonne pratique de programmation impose d'avoir des fichiers qui contiennent le même type des fonctions. Par exemple un fichier qui contient des fonctions qui effectuent des calculs, un autre avec des fonctions d'écriture ou d'affichage, etc.
La bonne pratique de programmation impose aussi que chaque fonction ne fait qu'une seule chose. Par exemple une fonction fait un calcul mais l'affichage du résultat de ce calcul doit se faire à l'extérieur de cette fonction, à l'aide d'une autre fonction spécialisée à l'affichage.
Ainsi un programme acquiert une forme très modulaire, sa conception se trouve facilitée et aussi sa maintenance.
STYLES DE PROGRAMMATION


L'écriture d'un code est pour beaucoup de personnes l'occasion de montrer leurs penchants artistiques, de laisser libre cours à leur imagination ou encore d'exercer leur dexterité intellectuelle. Or rien n'est plus desastreux. En effet la fabrication du logiciel est une pratique industrielle et comme telle elle doit obéir à des normes et utiliser des techniques codifiées.  En règle générale, lorsqu'on écrit un programme nous devons avoir constamment à l'esprit deux préoccupations : économiser le temps d'exécution et économiser l'espace mémoire. Bien sûr ceci ne doit pas se faire au détriment de la lisibilité du code. Le code doit rester toujours lisible.
Dans ce paragraphe on examinera des styles de programmation qui permettent de minimiser le temps d'exécution et le prochain paragraphe sera consacré à la minimisation de la mémoire utilisée.
Scilab est un langage vectoriel ce qui signifie qu'un calcul se fait plus rapidement en vectorisant qu'en faisant des boucles. Le programme suivant présente trois façons de calculer la somme des éléments d'un tableau et il utilise la fonction timer() qui fournit le temps écoulé entre deux appels consécutifs à cette fonction.

// Test du temps d'exécution
clear(); x=rand(1,900000);
// Somme élément par élément
timer(); s1 = 0; for i= 1:900000 s1 = s1 + x(i); end t1 =timer(); disp(t1,"Temps somme élément par élément");
// Produit scalaire
timer(); y = ones(900000,1); s2 = x*y; t2 =timer(); disp(t2,"Temps somme par produit scalaire");
// Somme cumulative
timer(); s3=cumsum(x); // cumsum calcul somme cumulée éléments de x t3 =timer(); disp(t3,"Temps somme cumulée");

Les résultats de l'exécution sont les suivants :

Temps somme élément par élément
6.9299648
Temps somme par produit scalaire
0.0901296
Temps somme cumulée
0.0600864
exec done



On en conclut que
Un tableau à deux dimensions est considéré par Scilab comme un tableau à une dimension. En effet Scilab stocke d'abord dans un tableau la première colonne du tableau bidimensionnel, dans la suite de la première colonne, la deuxième colonne et aini de suite. Nous avons donc intérêt, dans la mesure du possible, de tarvailler de façon vectorisée sur les colonnes que sur les lignes. Ainsi le programme suivant

// Test du temps d'exécution sur un tableau à 2D
clear(); x=rand(3000,3000);
// Somme par extraction des colonnes
y = ones(1,3000); timer(); s1 = 0; for j=1:3000 s1 =s1 + y * x(:,j); end t1 = timer(); disp(t1,"Temps somme par colonnes");
// Somme par extraction des lignes
y = y'; timer(); s2 = 0; for i= 1:3000 s2 = s2 + x(i,sourire * y; end t2 =timer(); disp(t2,"Temps somme par lignes");

Les résultats sont les suivants

Temps somme par colonnes 0.3605184
Temps somme par lignes 0.5708208
exec done



Nous constatons que nous avons une augmentation du temps de calcul de plus de 50% si on travaille sur les lignes que sur les colonnes.


Il y a en Scilab des outils de "profiling" qui permettent de deceler les instructions les plus gourmandes en temps d'exécution. Pour utiliser ces outils il faut à l'invocation des fonctions utiliser l'option "p". Reprenons l'exemple précédent. Nous construisons le fichier somme.sci qui contient les fonctions de calcul :

function s1 = somme(x)
// Somme élément par élément
s1 = 0; for i= 1:900000 s1 = s1 + x(i); end
endfunction
function s2 = sommePS(x)
// Produit scalaire
y = ones(900000,1); s2 = x*y;
endfunction
function s3 = sommeCum(x)
// Somme cumulative
s3=cumsum(x);
endfunction

L'exécution du programme principal suivant :

// Test du temps d'exécution
clear(); getf('somme.sci','p');
x=rand(1,900000); s1 = somme(x); profile(somme)

donne les résultats suivants2


Ligne Nb exec Temps Interp
function s1 = somme(x) 1 0 0
s1 = 0; 1 0 2
for i= 1:900000 900000 0.00107 0
s1 = s1 + x(i); 900000 0.003447 6
end 1 0 0
endfunction 1 0 0


La première colonne indique le nombre d'exécutions de la ligne, la deuxième le temps cumulé et la troisième l'effort d'interprétation pour une exécution de la ligne correspondante. On peut aussi la fonction showprofile(nomFonction) qui fournit aussi le texte du programme, mais le temps cumulé est mis à zéro.
TECHNIQUES DE PROGRAMMATION


L'autre préoccupation majeure de la programmation est la minimisation de mémoire. Il n'y a pas de méthodes générales qui permettent de reduire la quantité de la mémoire utilisée. Tout au plus nous pouvons énumérer quelques principes qui contribuent à l'écriture des programmes "économiques".


LES FICHIERS


Si on crée une matrice a et un vecteur b, on peut le stocker dans un fichiers des données comme suit :

-->a=[1 2 3; -3 4 5]; b=1:10;

-->save('donnees.txt',a,b)




On peut lire ce fichier

-->clear()

-->load('donnes.txt')

-->a

a =

1. 2. 3.

- 3. 4. 5.

-->b

b =

1. 2. 3. 4. 5. 6. 7. 8. 9. 10.



On peut aussi stocker des valeurs numériques formatées :

-->u=file('open','data.dat','new');

-->write(u,a,'(3f10.6)')

-->file('close',u)



Pour la lecture on a

-->clear()

-->u=file('open','data.dat','old');

-->a=read(u,-1,3)

a =

1. 2. 3.

- 3. 4. 5.

-->file('close',u)  // Ne pas oublier de fermer le fichier




EXERCICES


EXERCICE 1.- Écrire un programme qui inverse les éléments d'une matrice m×n .
Exemple : La matrice
[1
3
9
2
-1
1]
donnera
[1
-1
2
9
3
1]


EXERCICE 2.- Edward Lorenz a decouvert les processus chaotiques en étudiant le comportement d'un système gazeux. Pour son étude Lorenz a simplifié les équations de Navier-Stockes et il a aboutit au système suivant de trois équations différentielles ordinaires :
dx

dt
=
-ax+ay
dy

dt
=
-xz+rx-y
dz

dt
=
xy-bz
où a,b et r sont des constantes dont les valeurs déterminent le comportement du système. Pour avoir un comportement chaotique on prend
a=10,b= 8

3
,r=28
Le but de cet exercice est de résoudre le système et de tracer sa trajectoire en deux et trois dimensions.
Travail à faire :
  1. On écrit d'abord  la fonction lorenz qui reproduit les trois équations ci-dessus. Comme cette fonction doit être utilisée par la fonction de Scilab ode, qui fournit la résolution des systèmes d'équations différentielles, sa définition doit être du type
    ydot = lorenz(t,y)
    où t est un vecteur avec les instants du temps où la solution sera calculée, y vecteur des points de la solution et ydot est le vecteur de dérivées de x,y et z par rapport à t.
  2. On écrit ensuite le programme principal. 
    1. On stocke dans un vecteur colonne que l'on peut nommer y0 les valeurs initiales :
      • x(0) = -3.2917495672888E+00
      • y(0) = -6.3058819810691E+00
      • z(0) = 8.1821792963329E+00
    2. On calcule les instants d'intégration : on prend t0 = 0 et tf = 50 et comme pas d'intégration dt = 0.01. Les instants d'intégration sont donnés par l'instruction
      t=t0 : dt : tf;
    3. Ensuite on appelle la résolution du système par l'instruction
      y  ode(y0, t0, t, lorenz);
      et on récupère dans y la trajectoire selon les trois dimensions.
    4. On trace les trajectoires en deux dimensions selon les plans x-y, x-z et y-z.
    5. On trace la trajectoire en trois dimensions.

Footnotes:

1Les crochets [ et ] englobent un contenu optionnel.
2Pour la meilleure compréhension des résultats, nous avons répéter le code de la fonction.


File translated from TEX by TTH, version 3.76.
On 04 Oct 2006, 01:07.