Scilab : un logiciel libre pour le calcul
scientifique
troisième partie :
<<Structure de données et surcharge d'opérateurs.>>
Tous ces articles en ligne sur Scilab ont été écrits pour
LINUX MAGAZINE FRANCE par les
développeurs du Scilab Group. Les articles sont sous
licence FDL (Free Documentation
Licence)
Un langage matriciel, la possibilité de définir de nouveaux
objets et des mécanismes de surcharge des opérateurs et des
fonctions font de Scilab un langage de programmation puissant permettant
d'écrire des programmes complexes de façon lisible et
compacte.
Nous avons évoqué dans les premiers articles les objets de base
de Scilab (matrices de nombres, de polynômes, de chaînes de
caractères et de booléens). Mais pour développer des
programmes complexes avec un code lisible et concis, le programmeur a besoin de
structures pouvant contenir des données
hétérogènes. Scilab permet de définir et de
manipuler ces nouveaux objets aisément. Par exemple :
-->tableau=list(['x';'y'],['a','b','c'],rand(2,3))
crée une structure de données représentant un tableau de
nombres auquel est associé des noms de lignes et de colonnes. Les
différents champs de cette structure sont accessibles par leur rang dans
la structure.
-->tableau(2)
ans=!a b c !
-->sum(tableau(3)(1,:))
ans=1.1943134
On note ici l'usage du symbole << :
>> qui désigne l'ensemble des lignes de la
matrice.
Il est bien évidemment possible de changer la valeur d'un champ
-->tableau(3)=[1 2 3;4 5 6];
-->tableau(3)
ans=! 1. 2. 3. ! ! 4. 5. 6. !
voire même d'une sous-matrice contenue dans l'un des champs
-->tableau(3)(:,2)=[0;0];
-->tableau(3)
ans=! 1. 0. 3. ! ! 4. 0. 6. !
Ce type de structure peut être récursif
définissant ainsi des structures de données arborescentes, comme
dans l'exemple ci-dessous où l'on veut décrire l'arbre des
descendants d'une personne. Un individu y est décrit par une liste
contenant son état civil (prénom, date de naissance, liste des
enfants). Cette structure est créee par la fonction etat_civil
.
function individu=etat_civil(prenom,date_naissance,enfants)
[lhs,rhs]=argn()
if rhs==2 then enfants=list(),end
individu=list(prenom,date_naissance,enfants)
On note ici l'usage de la fonction argn qui permet de
connaître le nombre effectif d'arguments de sortie et d'entrée
passés lors de l'appel de la fonction.
Exemple d'utilisation :
-->JeanPierre=etat_civil('Jean-Pierre',1908)
JeanPierre=
JeanPierre(1)
Jean-Pierre
JeanPierre(2)
1908.
JeanPierre(3)
()
On observe ci-dessus la visualisation par défaut des structures.
La fonction récursive nouveau ajoute un nouveau descendant
à un ascendant spécifié par son prénom.
function l=nouveau(l,individu,ascendant)
if l(1)==ascendant then // L'ascendant designe a ete trouve
l(3)($+1)=individu // Extension de la liste des enfants
else
for k=1:size(l(3)) // Parcours de l'arbre des descendants
l(3)(k)=nouveau(l(3)(k),individu,ascendant)
end
end
Pour illustrer l'usage de ces fonctions, créons la descendance de Jean
Pierre :
arbre=etat_civil('Jean-Pierre',1908);// l'ancetre
// et sa descendance ...
arbre=nouveau(arbre,etat_civil('Maurice',1928),'Jean-Pierre');
arbre=nouveau(arbre,etat_civil('Francois',1930),'Jean-Pierre');
arbre=nouveau(arbre,etat_civil('Marianne',1953),'Francois');
arbre=nouveau(arbre,etat_civil('Claude',1960),'Maurice');
arbre=nouveau(arbre,etat_civil('Serge',1965),'Maurice');
arbre=nouveau(arbre,etat_civil('Jean-Philippe',1985),'Marianne');
La visualisation par défaut de la structure arbre n'est pas
très lisible, nous définissons donc la fonction récursive
affiche_arbre pour réaliser une visualisation
spécifique.
On peut noter ici l'usage du symbole <<+
>> pour la concaténation des chaînes, de la
fonction string qui convertit un nombre en une
chaîne de caractères et de la fonction exists
qui permet de savoir si une variable existe.
Cette fonction génere le résultat suivant :
-->affiche_arbre(arbre) Jean-Pierre 1908 Maurice 1928 Claude
1960 Serge 1965 Francois 1930 Marianne 1953 Jean-Philippe 1985 Avec de telles structures, le programmeur veut quelquefois
désigner les champs par des noms symboliques plutôt que par un
numéro. Cela peut être fait en utilisant les structures
<<typées>> créees par la fonction tlist. Ce
type de structure hérite de toutes les propriétés des
structures précédentes mais il est de plus possible d'y attacher
un type symbolique ainsi que de nommer les champs.
La fonction etat_civil peut être modifiée comme suit pour
utiliser ce type de structure :
function individu=etat_civil(prenom,date_naissance,enfants)
[lhs,rhs]=argn()
if rhs==2 then enfants=list(),end
individu=tlist(['etat','prenom','date','enfants'],..
prenom,date_naissance,enfants)
Le premier champ de la nouvelle structure individu est un vecteur de
chaînes de caractères. La première chaîne donne le
type symbolique de la structure et les suivants les noms symboliques des
champs. La fonction nouveau s'écrit alors de façon plus
lisible :
function l=nouveau(l,individu,ascendant)
if l.prenom==ascendant then // L'ascendant designe a ete trouve
l.enfants($+1)=individu // Extension de la liste des enfants
else
for k=1:size(l.enfants) // Parcours de l'arbre des descendants
l.enfants(k)=nouveau(l.enfants(k),individu,ascendant)
end
end
Et la fonction %etat_p ci-dessous adaptée de affiche_arbre
permet de surcharger la visualisation par défaut des structure de
type etat. Le nom la fonction (%etat_p) est construit
à partir du nom symbolique du type etat et du symbole du
display <<p>>.
function %etat_p(descendants)
if ~exists('shift') then shift='',end
// affiche le niveau courant
write(%io(2),shift+descendants.prenom+' '+string(descendants.date))
// accroit le decalage les descendants
shift=shift+' ';// decalage du texte relatif a ce niveau
for k=1:size(descendants.enfants) // boucle sur les descendants
// appel recursif de la fonction pour traiter les descendants
%etat_p(descendants.enfants(k))
end
Il n'est alors plus besoin d'appeler explicitement une fonction pour
réaliser la visualisation de la structure arbre :
-->arbre
Jean-Pierre 1908
Maurice 1928
Claude 1960
Serge 1965
Francois 1930
Marianne 1953
Jean-Philippe 1985
De la même façon, la sémantique de l'insertion peut
être surchargée en définissant la fonction %etat_i_etat
comme ci-dessous. Le nom de la fonction est contruit à partir du
nom symbolique des opérateurs (ici etat) et du symbole de
l'opérateur (ici <<i>> pour l'insertion).
function l=%etat_i_etat(ascendant,individu,l)
if l.prenom==ascendant then
l.enfants($+1)=individu
else
for k=1:size(l.enfants) // boucle sur les descendants
l.enfants(k)=nouveau(l.enfants(k),individu,ascendant)
end
end
La définition de la descendance se fait alors plus simplement :
arbre=etat_civil('Jean-Pierre',1908);// l'ancetre // et sa
descendance arbre('Jean-Pierre')=etat_civil('Maurice',1928);
arbre('Jean-Pierre')=etat_civil('Francois',1930);
arbre.Maurice=etat_civil('Claude',1960); arbre.Maurice=etat_civil('Serge',1965)
arbre.Francois=etat_civil('Marianne',1953)
arbre.Marianne=etat_civil('Jean-Philippe',1985)
On note ici l'usage de la syntaxe arbre('Jean-Pierre')
au lieu de la notation <<.>>
à cause du symbole opératoire <<-
>> inclut dans le prénom.
Toujours plus fort! Le programmeur veut maintenant définir des
matrices d'objets. Nous allons illustrer comment cela peut se faire sous Scilab
pour le calcul automatique de la dérivée d'une fonction Scilab
par surcharge d'opérateurs.
Étant donnée une fonction Scilab F réalisant
y=F(x) où x et y sont des vecteurs ou
des matrices, on veut pouvoir calculer la valeur de la fonction G
pour tout couple (x,z) ou z est une matrice de même
dimension que x et l un scalaire (pour les
connaisseurs, il s'agit du calcul de la dérivée directionnelle
dans la la direction z) en utilisant le code de la fonction Scilab F
.
Cela peut se faire tout simplement en remplaçant x par une
structure xdx contenant x et z et en surchargeant la
définition des opérateurs et des fonctions de base de Scilab pour
que les différents calculs définis dans la fonction F
évaluent la valeur de la dérivée de leur résutat
par rapport à l en même temps que la
valeur elle-même.
La fonction der ci-dessous permet de créer un nouveau type de
donnée qui contient la valeur d'une variable en un point x
ainsi que la valeur de la dérivée directionnelle dx.
function [xdx]=der(x,dx)
xdx=mlist(['der';'v';'dv'],x,dx)
On note ici l'usage de la fonction mlist qui permet
de créer des structures de données orientées matricielle
(ce qui veut dire que les syntaxe xdx(i) et
xdx(i,j) seront interprétées comme l'indexage de
sous-matrice de xdx).
Nous souhaitons appliquer cette méthode à la fonction
normalize :
function x=normalize(x)
for k=1 : size(x,2)
s=sum(x(:,k))
if s~=0 then
x(:,k)=x(:,k)/s,
end
end
Pour ne pas compliquer l'exposé, nous ne montrerons que les fonctions de
surcharges nécessaires. Commençons par les fonctions qui
surchargent l'extraction de sous-matrice
function [z]=%der_e (varargin)
if size(varargin)==2 then //extraction x(i)
[i,x]=varargin(:) ;
z=der(x.v(i),x.dv(i))
else // extraction
x(i,j) [i,j,x]=varargin(:) ; z=der(x.v(i,j),x.dv(i,j))
end
et l'insertion
function y=%der_i_der(varargin) if size(varargin)==3 then //
syntaxe y(i)=x [i,x,y]=varargin(:) y.v(i)=x.v ; // insertion pour le champ
valeur y.dv(i)=x.dv ;// insertion pour le champ derivee else // syntaxe
z(i,x)=y [i,j,x,y]=varargin(:) y.v(i,j)=x.v y.dv(i,j)=x.dv end On
note ici l'usage du mot clé varargin qui permet
de passer un nombre variable d'arguments d'entrée. La variable
varargin est la structure (de type list)
des arguments effectifs d'entrée.
Les fonctions de surcharge des fonctions size et sum
s'écrivent comme suit :
function m=%der_size(a,flag)
m=size(a.v,flag)
function z=%der_sum(x)
z=der(sum(x.v),sum(x.dv))
Bien évidemment la dérivée de la somme est égale
à la somme des dérivées.
En ce qui concerne les fonctions de surcharge des opérateurs <<
/>> et <<=>> le nom de la fonction est
construit à partir du nom de type de chaque opérande et du code
de l'opérateur qui est <<r>> pour la division
à droite et <<n>> pour la comparaison.
function z=%der_r_der
(x,y) r=x.v / y.v
z=der(r,(x.dv - r*y.dv)/ y.v )
La fonction de surcharge de la comparaison est beaucoup plus simple :
function z=%der_n_s(x,y)
z=x.v
y
Et miracle, sans rien changer à la fonction normalize, on
obtient
-->a=[1 2;3 4];
-->y=normalize(a)
y=
! 0.25 0.3333333 !
! 0.75 0.6666667 !
-->A=der(a,eye(a));
-->Y=normalize(A)
Y=
Y(1)
!der !
! !
!v !
! !
!dv !
Y(2)
! 0.25 0.3333333 !
! 0.75 0.6666667 !
Y(3)
! 0.1875 - 0.0555556 !
! - 0.1875 0.0555556 !
-->norm(Y.dv-(normalize(a+1.d-9*A.dv)-y)/1.d-9) //test
ans=
2.288D-08
Cet exemple est extrait d'une boîte à outil de
différentiation automatique de programmes en préparation. Par
ailleurs, un certain nombre de types de variable définis dans la version
2.5 de Scilab (matrice de fractions rationnelles, systèmes dynamiques
linéaires et matrices n-dimensionnelles sont codés par des
structures Scilab et les opérations associées définies par
surcharge d'opérateurs.
Par exemple, cela permet une syntaxe d'utilisation des matrices
n-dimensionnelles très proche de celle des matrices ordinaires.
-->a=[1 2;3 4];
-->a(:,:,2)=eye(2,2)
a=(:,:,1)
! 1. 2. !
! 3. 4. !
(:,:,2)
! 1. 0. !
! 0. 1. !
-->[a a]
ans=(:,:,1)
! 1. 2. 1. 2. !
! 3. 4. 3. 4. !
(:,:,2)
! 1. 0. 1. 0. !
! 0. 1. 0. 1. !
Par ailleur, le traducteur Matlab -> Scilab, entièrement écrit
en Scilab, utilise massivement ce type de structures. Pour ménager le
suspense nous n'avons parlé ici que de la surcharge des
opérateurs et fonctions par la définition de fonctions
écrites en Scilab. Cela est aussi possible par la définition de
nouvelles primitives écrites en C ou en Fortran.
Pour en apprendre d'avantage sur les capacités de Scilab, rendez-vous au
prochain numéro...
Serge Steer
Scilab Group
scilab@inria.fr
http://www-rocq.inria.fr/scilab/
Newsgroups:
comp.soft-sys.math.scilab
This document was translated from LATEX
by HEVEA.