Click on the link for the interactive version:
Consider the matrix equation Ax=b. where A=(1000−12−100−12−100−11), x=(x0x1x2x3) and b=(114120). Open the example code on my website click here, it will provide a template to implement the code outlined in this demonstration. The final solution is also on the web click here. Out aim is to implement SOR to find the solution x. The task will be split into several steps:
To solve this problem first we need to declare the appropriate libraries at the top of the code:
#include <iostream>
#include <cmath>
#include <vector>
using namespace std;
Inside the main function we need to set up the problem, we use three vectors a
, b
, c
for the diagonals of the matrix, and a vector rhs
for b.
// declare vector to store matrix A
vector<double> a = { 0.,-1,-1,-1};
vector<double> b = { 1.,2,2,1};
vector<double> c = { 0.,-1,-1,0.};
// declare vector for the rhs v and solution x
vector<double> rhs = {1.,0.25,0.5,0.};
vector<double> x;
Now we set up the matrix solver using a simple iteration scheme. The general code layout is outlined below, at the moment the code breaks after one step.
{
// initial guess for x
x = {0.,0.,0.,0.};
// MATRIX SOLVE
// sor variables
// set iterMax to a large enough value so that method converges
int iterMax=50;
double tol=1.e-6,omega=1;
// sor loop
for(int sor=0;sor<iterMax;sor++)
{
// implement sor in here
// x[0]=...
// output guess at (sor+1)th iteration
cout << "x = { ";
for(auto xi : x)cout << xi << " ";
cout << "} at iteration " << sor+1 << "\n";
// make an exit condition when solution found
// ...
break;
}
}
Now input the sor code, exploiting the tridiagonal structure of the matrix so simplify the process. We can see that the solution converges after about 50 steps (to 6sf).
{
// initial guess for x
x = {0.,0.,0.,0.};
// MATRIX SOLVE
// sor variables
// set iterMax to a large enough value so that method converges
int iterMax=50;
double tol=1.e-6,omega=1;
int sor=0;
// sor loop
for(sor=0;sor<iterMax;sor++)
{
// implement sor in here
x[0] = rhs[0]/b[0];
for(int j=1;j<3;j++)
{
x[j] = (rhs[j] - a[j]*x[j-1] - c[j]*x[j+1])/b[j];
}
x[3] = (rhs[3] - a[3]*x[2])/b[3];
// output guess at (sor+1)th iteration
cout << "x = { ";
for(auto xi : x)cout << xi << " ";
cout << "} at iteration " << sor+1 << "\n";
}
}
Next we want to know how to track convergence of our method. Often one of the best ways is to looks at a residual analysis, which is basically where we calculate r=b−Ax.
We have to do this after we have solved for all elements in the vector x.
{
// initial guess for x
x = {0.,0.,0.,0.};
// MATRIX SOLVE
// sor variables
// set iterMax to a large enough value so that method converges
int iterMax=50;
double tol=1.e-6,omega=1;
int sor=0;
// sor loop
for(sor=0;sor<iterMax;sor++)
{
vector<double> r(4);
// implement sor in here
x[0] = rhs[0]/b[0];
for(int j=1;j<3;j++)
{
x[j] = (rhs[j] - a[j]*x[j-1] - c[j]*x[j+1])/b[j];
}
x[3] = (rhs[3] - a[3]*x[2])/b[3];
// output guess at (sor+1)th iteration
if(sor%10==9)
{
cout << "x = { ";
for(auto xi : x)cout << xi << " ";
cout << "} and r = {";
cout << rhs[0] - b[0]*x[0] << " ";
cout << rhs[1] - a[1]*x[0] - b[1]*x[1] - c[1]*x[2] << " ";
cout << rhs[2] - a[2]*x[1] - b[2]*x[2] - c[2]*x[3] << " ";
cout << rhs[3] - a[3]*x[2] - b[3]*x[3] << " ";
cout << "} at iteration " << sor+1 << "\n";
}
}
}
Here we can see that the size of the residual is broadly similar to the error in the solution. As a result we can use the size of the residual (i.e. the norm) to measure how close we are to the solution. We introduce an error term to the calculation and check it against the tolerance. Further we also implement SOR, storing our new estimate in a variable y
so that we can generate a new value x[j] = x[j] + omega*(y-x[j])
.
{
// initial guess for x
x = {0.,0.,0.,0.};
// MATRIX SOLVE
// sor variables
// set iterMax to a large enough value so that method converges
int iterMax=500;
double tol=1.e-6,omega=1.4;
int sor=0;
// sor loop
for(sor=0;sor<iterMax;sor++)
{
double error=0.;
// implement sor in here
{
double y = rhs[0]/b[0];
x[0] = x[0] + omega*(y-x[0]);
}
for(int j=1;j<3;j++)
{
double y = (rhs[j] - a[j]*x[j-1] - c[j]*x[j+1])/b[j];
x[j] = x[j] + omega*(y-x[j]);
}
{
double y = (rhs[3] - a[3]*x[2])/b[3];
x[3] = x[3] + omega*(y-x[3]);
}
// calculate residual norm ||r|| as sum of absolute values
error += fabs(rhs[0] - b[0]*x[0]);
error += fabs(rhs[1] - a[1]*x[0] - b[1]*x[1] - c[1]*x[2]);
error += fabs(rhs[2] - a[2]*x[1] - b[2]*x[2] - c[2]*x[3]);
error += fabs(rhs[3] - a[3]*x[2] - b[3]*x[3]);
// make an exit condition when solution found
if(error<tol)
break;
}
cout << "Converged after " << sor+1 << " iterations.\n";
}
x
We can easily convert this into a function, which we can put in a separate file click here or above the main function. The function is called sorSolve(...)
where the arguments are a
, b
, c
, rhs
, x
, iterMax
, tol
, omega
and sor
. The definition of each parameter can be implied from the code above.
/*
ON INPUT:
a, b and c -- are the diagonals of the matrix
rhs -- is the right hand side
x -- is the initial guess
iterMax -- is maximum iterations
tol -- is the tolerance level
omega -- is the relaxation parameter
sor -- not used
ON OUTPUT:
a, b, c, rhs -- unchanged
x -- solution to Ax=b
iterMax, tol, omega -- unchanged
sor -- number of iterations to converge
*/
void sorSolve(const std::vector<double> &a,const std::vector<double> &b,const std::vector<double> &c,const std::vector<double> &rhs,
std::vector<double> &x,int iterMax,double tol,double omega,int &sor )
{
// assumes vectors a,b,c,d,rhs and x are same size (doesn't check)
int n=a.size()-1;
// sor loop
for(sor=0;sor<iterMax;sor++)
{
double error=0.;
// implement sor in here
{
double y = (rhs[0] - c[0]*x[1])/b[0];
x[0] = x[0] + omega*(y-x[0]);
}
for(int j=1;j<n;j++)
{
double y = (rhs[j] - a[j]*x[j-1] - c[j]*x[j+1])/b[j];
x[j] = x[j] + omega*(y-x[j]);
}
{
double y = (rhs[n] - a[n]*x[n-1])/b[n];
x[n] = x[n] + omega*(y-x[n]);
}
// calculate residual norm ||r|| as sum of absolute values
error += std::fabs(rhs[0] - b[0]*x[0] - c[0]*x[1]);
for(int j=1;j<n;j++)
error += std::fabs(rhs[j] - a[j]*x[j-1] - b[j]*x[j] - c[j]*x[j+1]);
error += std::fabs(rhs[n] - a[n]*x[n-1] - b[n]*x[n]);
// make an exit condition when solution found
if(error<tol)
break;
}
}
Once in a function we can play around to see the effect of the different input parameters. For instance, with defaults as above we get:
// reset initial guess for x
x = {0.,0.,0.,0.};
int sor;
// iterMax=50, tol=1.e-6, omega = 1
sorSolve(a,b,c,rhs,x,50,1.e-6,1.,sor);
cout << "Converged after " << sor+1 << " iterations.\n";
x
So with omega=1
we converge in 50 iterations. Now try again with omega=1.1
:
// reset initial guess for x
x = {0.,0.,0.,0.};
// this time omega = 1.1
sorSolve(a,b,c,rhs,x,50,1.e-6,1.1,sor);
cout << "Converged after " << sor+1 << " iterations.\n";
x
Now try it with a few different values
for(int varyOmega=5;varyOmega<=20;varyOmega++)
{
// reset initial guess for x
x = {0.,0.,0.,0.};
// this time omega = 1.1
double omega = varyOmega/10.;
sorSolve(a,b,c,rhs,x,5000,1.e-6,omega,sor);
cout << "Try with omega = " << omega << " we get x = { ";
for(auto xi : x)cout << xi << " ";
cout << "} after " << sor+1 << " iterations.\n";
}
As we can see the number of iterations is very dependent on the choice of omega. If we choose the value ω=1.4 the solver will converge around 2.5 times quicker, whilst choosing a value that is too big of ω=1.9 takes around 4 times longer. In fact, theoretically there is always an optimal choice of ω∈[1,2) that depend on the properties of the matrix A (the spectral radius). For some simple matrices this can be calculated easily but in general it is difficult to know.