TP 4
TP 4
TP 4
Objectifs et conseils
Qu’est ce que la POO ?
Qu’est ce qu’un objet ? Il s’agit d’un mélange de plusieurs variables et fonctions. Imaginez que
vous avez créé un programme qui permet de résoudre des équations différentielles ordinaires : vous
pouvez afficher vos solutions, calculer des ordres de convergence, comparer deux méthodes ...
Le code est complexe : il aura besoin de plusieurs fonctions qui s’appellent entre elles, ainsi
que de variables pour mémoriser la solution au cours du temps, la méthode utilisée, le pas de
temps choisi ... Au final, votre code est composé de plusieurs fonctions et variables. Votre code
sera difficilement accessible par quelqu’un qui n’est pas un expert du sujet : Quelle fonction
il faut appeler en premier ? Quelles valeurs doit-on envoyer à quelle fonction pour afficher la
solution ? etc ... Votre solution est de concevoir votre code de manière orientée objet. Ce qui
signifie que vous placerez tout votre code dans une grande boîte. Cette boîte c’est ce qu’on
appelle un objet. L’objet contient toutes les fonctions et variables mais elles sont masquées
pour l’utilisateur. Seulement quelques outils sont proposés à l’utilisateur comme par exemple :
définir mon pas de temps, mon intervalle de calcul et ma méthode, calculer l’ordre de la méthode,
afficher la solution ...
En quelques lignes :
• La programmation orientée objet est une façon de concevoir son code dans laquelle on ma-
nipule des objets.
• Les objets peuvent être complexes mais leur utilisation est simplifiée. C’est un des avantages
de la programmation orientée objet.
• Un objet est constitué d’attributs et de méthodes, c’est-à-dire de variables et de fonctions
membres.
• On appelle les méthodes de ces objets pour les modifier ou obtenir des informations.
Qu’est ce qu’une classe ? Pour créer un objet, il faut d’abord créer une classe. Créer une classe
consiste à définir les plans de l’objet. Une fois que la classe est faite (le plan), il est possible de
créer autant d’objets du même type. Vocabulaire : on dit qu’un objet est une instance d’une
classe.
Objectifs
• Surcharges d’opérateur et templates à travers l’implémentation d’une classe de vecteurs
creux,
• Utiliser une librairie extérieure pour les vecteurs denses et creux et les matrices denses et
creuses : Eigen.
Pour éviter de renvoyer un vecteur qui sera ensuite copié, on pourra renvoyer un const& :
std::vector<int> const& GetIndex() const;
4. Nous allons voir une fonctionnalité très importante du C++ : "la surcharge des opérateurs".
Cette technique permet de réaliser des opérations mathématiques entre les objets en utilisant les
symboles : +, −, ∗, ==, < et d’afficher un objet à l’aide de <<. Pour notre classe SparseVector
nous allons surcharger les opérateurs == et << afin d’utiliser les commandes suivantes :
cout << u << endl; //Affiche le vecteur u
//Compare les vecteurs u et v
if(u == v)
cout << "Les deux vecteurs sont égaux !" << endl;
L’affichage d’un vecteur creux doit écrire sur chaque ligne un élément non-nul, ce qui donne pour
le vecteur u :
5 1.3
7 2.1
10 3.3
11 2.5
Les surcharges d’opérateur se font à l’extérieur de la classe : les prototypes dans le fichier .h
se mettent après la fermeture de l’accolade et du point virgule et les définitions dans le .cpp se
mettent à la fin du fichier pour plus de clarté.
//Surchage de ==
bool operator==(Objet const& o1, Objet const& o2)
{
if (.....) // Effectuer la comparaison.
return true;
else
return false;
}
//Surchage de <<
ostream& operator<<(ostream& out, Objet const& o)
{
// Écrire le vecteur creux dans le flux "out" puis le renvoyer.
return out;
}
Implémenter ces 2 surcharges d’opérateur et les tester dans la fonction main en ajoutant :
v.Resize(2);
v.Index(0) = 3; v.Value(0) = 3.7; v.Index(1) = 5; v.Value(1) = 2.4;
cout << "u = " << u << endl;
cout << "v = " << v << endl;
cout << "w = " << w << endl;
if(u == v)
5. Pour finir cet exercice, nous allons voir une autre fonctionnalité du C++ : les "templates".
L’objectif est d’utiliser le même code pour un vecteur creux d’entiers, de réels ou de complexes.
La force des templates est d’autoriser une fonction ou une classe à utiliser des types différents.
Vous pouvez les reconnaitre par les chevrons < et > comme le fait la STL par exemple pour les
vecteurs :
vector<int> vecteur_int;
vector<double> vecteur_double;
// Inclure la librairie "complex" (encore un objet qui est aussi "templaté" !)
vector<complex <double> > vecteur_complex;
Nous souhaitons pouvoir faire pareil avec notre classe de vecteurs creux :
SparseVector<int> vecteur_creux_int;
SparseVector<double> vecteur_creux_double;
SparseVector<complex <double> > vecteur_creux_complex;
Et ensuite pour la définition de vos méthodes et vos opérateurs (dans le .cpp), l’adaptation est
rapide :
template<class T>
SparseVector<T>::SparseVector() {}
template<class T>
int SparseVector<T>::GetNumOfNonZeroElem() const
{
// Corps de la fonction x
}
template<class T>
ostream& operator<<(ostream& out, SparseVector<T> v)
{
// Corps de la fonction
}
C’est une erreur classique pour les templates. En effet, le compilateur a besoin de connaître "T"
au moment de la compilation pour générer la spécialisation. Plusieurs stratégies existent dans
la littérature et dépendent du contexte. Ici nous allons en voir deux. Copier le code dans deux
dossiers différents afin de pouvoir tester les deux stratégies.
6. La première stratégie est très utile quand on souhaite pouvoir utiliser n’importe quel T : int,
double, complex etc ... sans avoir à préciser la liste. Il suffit de tout mettre dans le ".h". Copier
donc le code qui est dans le ".cpp" à la fin de votre fichier ".h". Attention : si vous avez utilisé
using namespace std;
dans le ".cpp" vous devez modifier votre code. En effet il ne faut pas les inclure dans le ".h"
(page 4, TP 3). Supprimer le fichier ".cpp" et compiler seulement votre fichier ".cc".
Les deux surchages d’opérateur n’étant pas des méthodes de la classe, vous n’avez pas besoin de
conserver leurs prototypes dans le ".h". Les supprimer.
Créer un vecteur de réels, un vecteur d’entiers et un vecteur de complexes (ne pas oublier la
librairie complex ). Les afficher. Pour le vecteur de complexe, vous pouvez par exemple faire :
complex<double> z(3,1); // z = 3 + i
SparseVector<complex<double> > w;
w.Resize(1);
w.Index(0) = 2; w.Value(0) = z;
cout << "w = " << w << endl;
7. La deuxième stratégie consiste à définir à la fin du ".cpp" la liste des types que nous souhai-
tons :
template class SparseVector<double>;
template class SparseVector<int>;
template class SparseVector<complex <double> >; // ajouter #include <complex>
En ajoutant ces quelques lignes, il est possible de conserver les définitions des méthodes et des
opérateurs dans le ".cpp".
Cependant si vous ne voulez pas avoir à créer pour chaque type vos deux surcharges
d’opérateur, il faut les placer dans le ".h" (et supprimer alors leurs prototypes).
Créer un vecteur de réels, un vecteur d’entiers et un vecteur de complexes. Les afficher.
Télécharger deux fichiers en suivant ce lien : lien. Regarder ce que nous devons inclure pour
pouvoir utiliser Eigen en haut des fichiers.
1. Compiler le fichier main_dense.cc et l’exécuter. Regarder les commandes et leurs résultats
dans le terminal. Cette étape est très importante pour comprendre comment construire une
matrice dense. Des exemples de fonctionnalité sont aussi proposés. Bien sûr cette liste n’est
pas exhaustive donc il ne faut pas hésiter à chercher dans la documentation d’Eigen quand on
souhaite faire quelque chose qui n’est pas listé ici.
2. Compiler le fichier main_sparse.cc et l’exécuter. Regarder attentivement ce fichier pour com-
prendre comment définir une matrice creuse. Les dernières commandes qui sont proposées vous
montrent comment passer d’une matrice dense à une matrice creuse et vice-et-versa.
Eigen affiche une matrice creuse de deux manières différentes : tout d’abord une version creuse
et ensuite une version dense. L’affichage creux correspond au mode de stockage de la matrice
par Eigen. Par exemple la matrice T est stockée ainsi :
Nonzero entries:
(2,0) (-4,2) (-8,4) (2,1) (-4,3) (-8,5) (4,0) (2,2) (-4,4) (-8,6) (4,1) (2,3)
(-4,5) (-8,7) (8,0) (4,2) (2,4) (-4,6) (-8,8) (8,1) (4,3) (2,5) (-4,7) (-8,9)
(8,2) (4,4) (2,6) (-4,8) (8,3) (4,5) (2,7) (-4,9) (8,4) (4,6) (2,8) (8,5)
(4,7) (2,9)
Outer pointers:
0 3 6 10 14 19 24 28 32 35 $
(attention la fonction choisie doit bien vérifier les conditions aux bord !). Il faut donc prendre :
2. Afficher la solution approchée et la solution exacte avec Gnuplot pour N = 10, 100, 500 avec :
plot "solution.txt" using 1:2 title "solution approchee" with linespoints
replot "solution.txt" using 1:3 title "solution exacte" with linespoints
3. Nous souhaitons comparer les deux solveurs : Cholesky et Gradient conjugué. Vérifier que
nous obtenons les mêmes résultats quel que soit le solveur. À présent, nous allons les comparer
du point de vue du temps de calcul. Pour cela nous allons utiliser la librairie "chrono" (ne pas
oublier de l’ajouter) qui s’utilise de la façon suivante :
#include <chrono> // Au début du fichier
auto start = chrono::high_resolution_clock::now(); // Démarrage du chrono
//Action que je souhaite chronométrer
auto finish = chrono::high_resolution_clock::now(); // Fin du chrono
Conclure sur le solveur le plus adapté ici. Est-ce cohérent avec les conseils d’Eigen ?
Remarque : Vous avez sans doute remarqué que le type de start et finish est un type que
vous n’avez encore jamais vu. En fait auto n’est pas un type. Cela signifie automatique : on
laisse automatiquement le compilateur fixer le type de start et de finish. Pour cela les variables
définies avec auto doivent être absolument initialisées. Si nous ne souhaitons pas utiliser auto, il
faut aller voir dans la documentation de la librairie chrono quel est le type à utiliser (page) :
chrono::high_resolution_clock::time_point
Remplacer auto par ce dernier et vérifier que cela compile bien. En pratique, il ne faut pas abuser
du auto mais quand il n’y a aucune ambiguïté comme c’est le cas ici, il ne faut pas hésiter.
2. Nous allons proposer à l’utilisateur de choisir entre deux méthodes pour le calcul des valeurs
propres. Implémenter les deux méthodes suivantes :
• EigenSolver() : Nous allons utiliser le solveur SelfAdjointEigenSolver (page d’Eigen) puisque
notre matrice est symétrique. Il faut l’appliquer sur la version dense de la matrice de Laplace.
Ne pas oublier de remplir le vecteur _eigenvalues.
• LapackEigenSolver() : Nous allons utiliser la librairie Lapack (Fortran 90) qui est une
bibliothèque d’algèbre linéaire qui contient de nombreuses routines : résolution de systèmes
linéaires, ajustement par des moindres carrés linéaires, diagonalisation, méthodes spectrales,
nombreuses opérations de factorisation de matrices (QR, LU ...). Nous pouvons utiliser des
routines Lapack directement avec les matrices Eigen. Ici nous allons utiliser la routine dsyev
(pour les matrices symétriques) dont une explication est donnée ici : Lien Lapack (le nom
des routines rend parfois son utilisation difficile ...). Pour utiliser une routine il faut d’abord
exporter le prototype "C" de la fonction. Pour cela ajouter dans le fichier .cpp après les
include :
Pour pouvoir compiler vous devez créer un lien vers la librairie Lapack. Sous Linux il faut ajouter
la commande :
-llapack
lors de la compilation.
3. Implémenter la méthode SaveEigenSol. Les valeurs propres exactes sont données par :
k2 π2
λk = , k = 1, · · · , ∞.
(xmax − xmin )2