1. Méthodes Itératives Stationnaires Standards
Objectifs
- Implémenter toutes les méthodes itératives standards
- Comparer la vitesse de convergence des différentes méthodes
Algorithme générique
Pour résoudre le problème linéaire $A x= b$, une technique consiste à réécrire la matrice $A$ sous la forme $A = M - (M-A)$, où $M$ est une matrice bien choisie, et d’appliquer le schéma itératif suivant \begin{equation}\label{eq:split} Mx_{n+1} = (M-A)x_n + b \end{equation} à convergence, nous retrouvons bien $(M-(M-A))x = Ax = b$.
L’algorithme de résolution décrit par l’équation \eqref{eq:split} s’écrit alors, pour une tolérance $\varepsilon$ et un nombre maximal d’itérations $n_{max}$ donnés :
niter = 0 // nombre d'itérations
x = 0 // vecteur solution
r = b // vecteur résidu
resvec[0] = |r|/|b| // Résidu relatif de l'itération 0
while( (|r|/|b| > tolerance) && (niter < n_max))
y = M^{-1}*r // Calcul de la direction (résolution système)
x = x + y // Mise à jour de la solution courante
r = b - Ax // Mise à jour du résidu (vecteur)
niter = niter + 1 // Mise à jour du numéro de l'itération
resvec[niter] = |r|/|b| // Stockage du résidu relatif (à des fins d'analyse)
end
Les méthodes itératives que nous détaillons ici ne diffèrent que par le choix de la matrice $M$. Pour chacune d’entres elles, nous construirons une classe dédiée, bien qu’elles partagent toutes des codes similaires (nous pourrions utiliser l’héritage, mais cela compliquera la “templatisation”).
Méthodes standards
La matrice $M$ est choisie selon le splitting (découpe) d’une matrice $A$ comme $A = D - E - F$, où $D$, $E$ et $F$ sont des matrices de la même taille que $A$ et telles que (attention au signe des coefficents de $E$ et $F$) :
- $D = {\rm diag}(A)$ : Matrice ne contenant que les termes diagonaux de $A$
- $E$ : Matrice ne contenant que la partie triangulaire inférieure stricte de $-A$
- $F$ : Matrice ne contenant que la partie triangulaire supérieure stricte de $-A$
Ensuite, selon les choix pour la matrice $M$, la méthode obtenue sera différente :
Méthode | Matrice $M$ | Remarques |
---|---|---|
Jacobi | $D$ | $M^{-1} = D^{-1}$ est explicite |
Gauss-Seidel | $D - E$ | $M$ est triangulaire inférieure |
Relaxation | $\frac{1}{\omega}D - E$ | $M$ est triangulaire inférieure $0 < \omega < 2$ paramètre à contrôler |
Une Classe en Détail : Jacobi
Pour chaque méthode, nous proposons d’implémenter une classe distincte et donc, de créer deux fichiers (un en-tête et un source). Par exemple, pour Jacobi : include/jacobi.hpp
et src/jacobi.cpp
. Les méthodes se ressemblant, ces différentes classes se ressembleront naturellement. Nous détaillons ici la classe Jacobi
, cependant, pour Gauss-Seidel et la Relaxation, la classe sera similaire à quelques modifications près (par exemple la Relaxation nécessite un paramètre $\omega$ en plus).
Données Membres (ou paramètres)
Celles-ci seront séparées en deux, les données “entrantes”, fournies par l’utilisateur, et les données “sortantes”, calculées lors de la résolution du problème linéaire. En pratique, rien ne différencie ces deux types de données qui sont de type private
, seule leur utilisation permet de les distinguer :
- Les données entrantes sont fournies par l’utilisateur et ne sont pas modifiées par la classe
- Les données sortantes sont modifiées par la classe (vecteur solution, nombre d’itération, …) et seront récupérées par l’utilisateur à des fins d’analyses
Nous proposons les paramètres suivants, mais vous pouvez bien entendu en ajouter !
I/O | Type | Nom (suggestion) | Fonction |
---|---|---|---|
Entrée | const Matrice & |
A_ |
Matrice (dense) du système. |
Entrée | const Vecteur & |
b_ |
Vecteur (membre de droite) |
Entrée | double |
tol_ |
Tolérance |
Entrée | int |
n_max_ |
Nombre maximum d’itérations |
Sortie | Vecteur |
x_ |
Vecteur solution |
Sortie | int |
niter_ |
Nombre d’itérations |
Sortie | std::vector<double> |
resvec_ |
Tableau des normes des résidus relatifs (RESidual VECtor) |
La figure suivante illustre ces paramètres et la classe Jacobi
:
Constructeurs
À vous de décider ce dont vous avez besoin : un constructeur vide ? Un qui prend toutes les données d’entrée en argument, par exemple :
Jacobi(const Matrice &A, const Vecteur &b, double tol, int maxit);
Pour gagner en efficacité et limiter le coût mémoire, il est plus intéressant de ne pas stocker les Matrice et Vecteur, mais plutôt leur adresse (référence ou pointeur) d’où un paramètre de type const Matrice &
et const Vecteur &
. Cependant, attention, l’affectation de cette valeur doit s’effectuer avant l’entrée dans le constructeur à l’aide d’une liste d’initialisation :
Jacobi(const Matrice & A, const Vecteur & b,...)
:A_(A), b_(b)... // Liste d'initialisation
{
blabla
}
Notez que l’utilisation de listes d’initialisation est une très bonne pratique, y compris pour les autres paramètres.
Méthode Solve()
Outre les accesseurs (getter) et les mutateurs (setter) habituels pour respectivement accéder et modifier les paramètres, nous avons au moins besoin d’une fonction membre qui résout le système linéaire en appliquant l’algorithme de résolution. Celle-ci aura (probablement) le prototype suivant :
void Jacobi::Solve();
D’autre part, voici quelques propositions pour définir la méthode :
- La solution sera stockée dans
x_
- Le nombre d’itérations sera stocké dans
niter_
- À chaque itération, la norme du résidu relatif $\frac{\|r\|}{\|b\|}$ doit être calculé pour vérifier si la convergence est atteinte ou non. Cette quantité sera stockée dans le tableau
resvec_
au fur et à mesure des itérations. Ceci nous sera utile pour la partie analyse.
Vecteur
pour ajouter des fonctionnalités comme par exemple, le calcul de sa norme.
Vous ne devez surtout pas extraire la diagonale de la matrice dans une nouvelle Matrice
: si $A$ est une matrice prenant 4Go de mémoire, la duppliquer n’apparait pas comme une solution très rusée.
Pour un vecteur $x$, calculer $(\text{diag}(A))^{-1}x$ est très simple : il suffit de parcourir les éléments diagonales de $A$. Vous pouvez créez une nouvelle fonction résolvant un système diagonal, dans la même lignée des fonctions utilisées pour un système triangulaire.
Implémentation et Test
Pour tester et valider votre code, utiliser la même matrice que pour la factorisation LU :
$$
\begin{pmatrix}
2 & -1 & 0 & 0 &0\\\
-1 & 2 & -1 & 0 &0\\\
0 & -1 & 2 & -1 &0\\\
0 & 0& -1 & 2 & -1 \\\
0 & 0& 0 &-1 & 2 \\\
\end{pmatrix} X=
\begin{pmatrix}
1 \\\
1 \\\
1 \\\
1 \\\
1 \\\
\end{pmatrix}.
$$
La solution exacte de ce problème est $X = [2.5, 4,4.5, 4,2.5]^T$.
Construisez une telle classe Jacobi
. N’oubliez surtout pas de :
- Testez la compilation
- Testez le résultat de votre implémentation sur un cas petit et simple.
- Comparez la solution obtenue avec Jacobi avec celle obtenue par résolution directe.
- Tant que les trois points ci-dessus ne sont pas validés, ne passez pas à la suite !
Classe Gauss-Seidel
Contrairement à la méthode de Jacobi, la méthode de Gauss-Seidel requière la résolution d’un système linéaire triangulaire inférieur :
- Surtout n’extrayez pas la partie triangulaire inférieure dans une nouvelle
Matrice
! Pire encore, ne l’inversez en aucun cas ! - Soyons astucieux et utilisons la fonction résolvant un système linéaire triangulaire inférieur déjà implémentée !
Classe Relaxation
Deux remarques d’importance :
- La méthode nécessite un paramètre $\omega$ : vous pouvez bien entendu ajouter ce paramètre à votre classe
Relaxation
- Cette méthode nécessite la résolution d’un système linéaire triangulaire inférieure. La même remarque que pour Gauss-Seidel s’applique ici : surtout pas d’extraction de matrice ! Vous pouvez créer une nouvelle fonction qui résout un système linéaire triangulaire inférieur et dont la diagonale est multipliée par un paramètre (ici : $1/\omega$) !