Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Jacobi Method

guides/jacobi_method.livemd

Jacobi Method

metnum_url = "https://github.com/santiago-imelio/metnum.git"

Mix.install([
  {:metnum, git: metnum_url, branch: "main"},
  {:exla, "~> 0.7.3"},
  {:tucan, "~> 0.3.1"},
  {:kino_vega_lite, "~> 0.1.13"}
])

Nx.global_default_backend(EXLA.Backend)
Nx.Defn.global_default_options(compiler: EXLA)

Introduction

The Jacobi Method is an iterative algorithm that approximates the solution of a strictly diagonally dominant system of linear equations. In each iteration, it will approximate the solution of the system using the previous approximation. When two contiguous approximations are close enough, we say the algorithm has converged to the desired solution.

Given a system of linear equations $Ax = b$ of $n \times n$, the Jacobi method can be expressed as the sequence of approximations ${xi}{k \in \mathbb{N}}$ where each term can be calculated as

$$ xi^{(k+1)} = \frac{1}{a{ii}} \left( bi - \sum{\substack{j=1 \ j \neq i}}^n a_{ij} x_j^{(k)} \right), \forall i \in {1,…,n} $$

The coefficient matrix $A$ must be strictly diagonally dominant. That is

$$ |a{ii}| > \sum{\substack{j=1 \ j \neq i}}^n |a_{ij}|, \forall i $$

Example

We will demonstrate our implementation with the following system of linear equations

$$ \begin{cases} 2x_1 - x_2 = 1\ -x_1 + 3x_2 - x_3 = 2 \ -x_2 + 4x_3 - x_4 = 3 \ -x_3 + 5x_4 = 4 \end{cases} $$

The coefficients matrix $A$ is strictly diagonally dominant, therefore applying Jacobi will converge.

a =
  Nx.tensor([
    [2, -1, 0, 0],
    [-1, 3, -1, 0],
    [0, -1, 4, -1],
    [0, 0, -1, 5]
  ])

b = Nx.tensor([1, 2, 3, 4])
x_0 = Nx.tensor([0, 0, 0, 0])

:ok
:ok

We run the Jacobi method with the initial approximation $x_0 = (0,0,0,0)$.

alias Metnum.LinearEquations

epochs = 15
opts = [max_epochs: epochs, solver: :jacobi, sequence: true]

{jacobi_seq, _} = LinearEquations.solve(a, b, x_0, opts)
{#Nx.Tensor<
   f32[15][4]
   EXLA.Backend
   [
     [0.0, 0.0, 0.0, 0.0],
     [0.5, 0.6666666865348816, 0.75, 0.800000011920929],
     [0.8333333730697632, 1.0833333730697632, 1.1166666746139526, 0.949999988079071],
     [1.0416667461395264, 1.3166667222976685, 1.2583333253860474, 1.0233333110809326],
     [1.1583333015441895, 1.433333396911621, 1.3350000381469727, 1.0516666173934937],
     [1.2166666984558105, 1.4977778196334839, 1.371250033378601, 1.0670000314712524],
     [1.2488889694213867, 1.5293055772781372, 1.391194462776184, 1.0742499828338623],
     [1.2646527290344238, 1.5466943979263306, 1.4008889198303223, 1.078238844871521],
     [1.2733471393585205, 1.555180549621582, 1.406233310699463, 1.0801777839660645],
     [1.277590274810791, 1.5598602294921875, 1.4088395833969116, 1.0812466144561768],
     [1.2799301147460938, 1.562143325805664, 1.4102766513824463, 1.0817679166793823],
     [1.281071662902832, 1.5634021759033203, 1.410977840423584, 1.0820553302764893],
     [1.2817010879516602, ...],
     ...
   ]
 >,
 #Nx.Tensor<
   s64
   EXLA.Backend
   16
 >}

As shown here, the method starts converging towards a solution after several iterations. A conventional way of measuring how the algorithm converges is by using the norm of the error of each increment:

$$ ||e^{(k)}|| = ||x^{(k)}-x^{(k-1)}|| $$

real_solution = LinearEquations.solve(a, b)
#Nx.Tensor<
  f32[4]
  EXLA.Backend
  [1.2823528051376343, 1.5647058486938477, 1.4117647409439087, 1.082352876663208]
>
norms =
  jacobi_seq
  |> Nx.vectorize(:steps)
  |> Nx.LinAlg.norm(ord: 2)
  |> Nx.devectorize(keep_names: false)
  |> Nx.to_list()

errors =
  Nx.subtract(jacobi_seq[1..(epochs - 1)], jacobi_seq[0..(epochs - 2)])
  |> Nx.vectorize(:deltas)
  |> Nx.LinAlg.norm(ord: 2)
  |> Nx.devectorize(keep_names: false)
  |> Nx.to_list()

real_solution_norm =
  real_solution
  |> Nx.LinAlg.norm(ord: 2)
  |> Nx.to_number()

:ok
:ok
data_norms = [norm: norms, epoch: 0..(epochs - 1)]
data_errors = [error: errors, epoch: 1..(epochs - 1)]

norm_plot_opts = [title: "2-norm by epoch", height: 300, width: 400, points: true]
hruler_opts = [line_color: "red", stroke_width: 2, stroke_dash: [3]]

error_plot_opts = [
  title: "Evolution of iteration error",
  height: 300,
  width: 300,
  points: true,
  line_color: "green",
  point_color: "green"
]

plot =
  Tucan.hconcat([
    Tucan.lineplot(data_norms, "epoch", "norm", norm_plot_opts)
    |> Tucan.hruler(real_solution_norm, hruler_opts),
    Tucan.lineplot(data_errors, "epoch", "error", error_plot_opts)
  ])

# Comment to visualize with KinoVegaLite
# :ok
Evolution of the norm of and incremental error of each approximation
Evolution of the norm and incremental error of each approximation