Triangular systems

In [1]:
versioninfo()
Julia Version 1.7.1
Commit ac5cc99908 (2021-12-22 19:35 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin19.5.0)
  CPU: Intel(R) Core(TM) i7-6920HQ CPU @ 2.90GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-12.0.1 (ORCJIT, skylake)
Environment:
  JULIA_EDITOR = code
  JULIA_NUM_THREADS = 4

For the next couple of lectures, we consider computer algorithms for solving linear equations $\mathbf{A} \mathbf{x} = \mathbf{b}$, a ubiquitous task in statistics.

Key idea: turning original problem into an easy one, e.g., triangular system.

Lower triangular system

To solve $\mathbf{A} \mathbf{x} = \mathbf{b}$, where $\mathbf{A} \in \mathbb{R}^{n \times n}$ is lower triangular

$$ \begin{pmatrix} a_{11} & 0 & \cdots & 0 \\ a_{21} & a_{22} & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nn} \end{pmatrix} \begin{pmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{pmatrix} = \begin{pmatrix} b_1 \\ b_2 \\ \vdots \\ b_n \end{pmatrix}. $$
  • Forward substitution: $$ \begin{eqnarray*} x_1 &=& b_1 / a_{11} \\ x_2 &=& (b_2 - a_{21} x_1) / a_{22} \\ x_3 &=& (b_3 - a_{31} x_1 - a_{32} x_2) / a_{33} \\ &\vdots& \\ x_n &=& (b_n - a_{n1} x_1 - a_{n2} x_2 - \cdots - a_{n,n-1} x_{n-1}) / a_{nn}. \end{eqnarray*} $$

  • $1 + 3 + 5 + \cdots + (2n-1) = n^2$ flops.

  • $\mathbf{A}$ can be accessed by row ($ij$ loop) or column ($ji$ loop).

Upper triangular system

To solve $\mathbf{A} \mathbf{x} = \mathbf{b}$, where $\mathbf{A} \in \mathbb{R}^{n \times n}$ is upper triangular
$$ \begin{pmatrix} a_{11} & \cdots & a_{1,n-1} & a_{1n} \\ \vdots & \ddots & \vdots & \vdots \\ 0 & \cdots & a_{n-1,n-1} & a_{n-1,n} \\ 0 & 0 & 0 & a_{nn} \end{pmatrix} \begin{pmatrix} x_1 \\ \vdots \\ x_{n-1} \\ x_n \end{pmatrix} = \begin{pmatrix} b_1 \\ \vdots \\ b_{n-1} \\ b_n \end{pmatrix}. $$

  • Back substitution $$ \begin{eqnarray*} x_n &=& b_n / a_{nn} \\ x_{n-1} &=& (b_{n-1} - a_{n-1,n} x_n) / a_{n-1,n-1} \\ x_{n-2} &=& (b_{n-2} - a_{n-2,n-1} x_{n-1} - a_{n-2,n} x_n) / a_{n-2,n-2} \\ &\vdots& \\ x_1 &=& (b_1 - a_{12} x_2 - a_{13} x_3 - \cdots - a_{1,n} x_{n}) / a_{11}. \end{eqnarray*} $$

  • $n^2$ flops.

  • $\mathbf{A}$ can be accessed by row ($ij$ loop) or column ($ji$ loop).

Implementation

  • BLAS level 2 function: trsv (triangular solve with one right hand side).

  • BLAS level 3 function: trsm (matrix triangular solve, i.e., multiple right hand sides).

  • Julia

    • The left divide \ operator in Julia is used for solving linear equations or least squares problem.
    • If A is a triangular matrix, the command A \ b uses forward or backward substitution
    • Or we can call the BLAS wrapper functions directly: trsv!, trsv, trsm!, trsm
In [2]:
using LinearAlgebra, Random

Random.seed!(123) # seed
n = 5
A = randn(n, n)
b = randn(n)
# a random matrix
A
Out[2]:
5×5 Matrix{Float64}:
  0.808288   0.229819    1.21928    -0.890077   2.00811
 -1.12207   -0.421769    0.292914    0.854242   0.76503
 -1.10464   -1.35559    -0.0311481   0.341782   0.180254
 -0.416993   0.0694591   0.315833   -0.31887    2.02891
  0.287588  -0.117323   -2.16238    -0.337454  -1.08822
In [3]:
Al = LowerTriangular(A) # does not create extra matrix
Out[3]:
5×5 LowerTriangular{Float64, Matrix{Float64}}:
  0.808288    ⋅           ⋅           ⋅          ⋅ 
 -1.12207   -0.421769     ⋅           ⋅          ⋅ 
 -1.10464   -1.35559    -0.0311481    ⋅          ⋅ 
 -0.416993   0.0694591   0.315833   -0.31887     ⋅ 
  0.287588  -0.117323   -2.16238    -0.337454  -1.08822
In [4]:
dump(Al)
LowerTriangular{Float64, Matrix{Float64}}
  data: Array{Float64}((5, 5)) [0.8082879284649668 0.2298186980518676 … -0.8900766562698114 2.008112624852031; -1.1220725081141734 -0.4217686643996927 … 0.8542424475591821 0.7650300179102025; … ; -0.4169926351649334 0.0694591410918936 … -0.3188700715740966 2.0289114854525314; 0.28758798062385577 -0.11732280453081337 … -0.33745447783309457 -1.088218513936287]
In [5]:
Al.data
Out[5]:
5×5 Matrix{Float64}:
  0.808288   0.229819    1.21928    -0.890077   2.00811
 -1.12207   -0.421769    0.292914    0.854242   0.76503
 -1.10464   -1.35559    -0.0311481   0.341782   0.180254
 -0.416993   0.0694591   0.315833   -0.31887    2.02891
  0.287588  -0.117323   -2.16238    -0.337454  -1.08822
In [6]:
# same data
pointer(Al.data), pointer(A)
Out[6]:
(Ptr{Float64} @0x000000011bde4c40, Ptr{Float64} @0x000000011bde4c40)
In [7]:
Al \ b # dispatched to BLAS function for triangular solve
Out[7]:
5-element Vector{Float64}:
    0.8706777634658246
   -2.6561704782961275
   79.95736189623058
   74.31241954954415
 -181.43591336155936
In [8]:
# or use BLAS wrapper directly
BLAS.trsv('L', 'N', 'N', A, b)
Out[8]:
5-element Vector{Float64}:
    0.8706777634658246
   -2.6561704782961275
   79.95736189623058
   74.31241954954415
 -181.43591336155936
In [9]:
?BLAS.trsv
Out[9]:
trsv(ul, tA, dA, A, b)

Return the solution to A*x = b or one of the other two variants determined by tA and ul. dA determines if the diagonal values are read or are assumed to be all ones.

Some algebraic facts of triangular system

  • Eigenvalues of a triangular matrix $\mathbf{A}$ are diagonal entries $\lambda_i = a_{ii}$.

  • Determinant $\det(\mathbf{A}) = \prod_i a_{ii}$.

  • The product of two upper (lower) triangular matrices is upper (lower) triangular.

  • The inverse of an upper (lower) triangular matrix is upper (lower) triangular.

  • The product of two unit upper (lower) triangular matrices is unit upper (lower) triangular.

  • The inverse of a unit upper (lower) triangular matrix is unit upper (lower) triangular.

Julia types for triangular matrices

LowerTriangular, UnitLowerTriangular, UpperTriangular, UnitUpperTriangular.

In [10]:
A
Out[10]:
5×5 Matrix{Float64}:
  0.808288   0.229819    1.21928    -0.890077   2.00811
 -1.12207   -0.421769    0.292914    0.854242   0.76503
 -1.10464   -1.35559    -0.0311481   0.341782   0.180254
 -0.416993   0.0694591   0.315833   -0.31887    2.02891
  0.287588  -0.117323   -2.16238    -0.337454  -1.08822
In [11]:
LowerTriangular(A)
Out[11]:
5×5 LowerTriangular{Float64, Matrix{Float64}}:
  0.808288    ⋅           ⋅           ⋅          ⋅ 
 -1.12207   -0.421769     ⋅           ⋅          ⋅ 
 -1.10464   -1.35559    -0.0311481    ⋅          ⋅ 
 -0.416993   0.0694591   0.315833   -0.31887     ⋅ 
  0.287588  -0.117323   -2.16238    -0.337454  -1.08822
In [12]:
LinearAlgebra.UnitLowerTriangular(A)
Out[12]:
5×5 UnitLowerTriangular{Float64, Matrix{Float64}}:
  1.0         ⋅           ⋅          ⋅         ⋅ 
 -1.12207    1.0          ⋅          ⋅         ⋅ 
 -1.10464   -1.35559     1.0         ⋅         ⋅ 
 -0.416993   0.0694591   0.315833   1.0        ⋅ 
  0.287588  -0.117323   -2.16238   -0.337454  1.0
In [13]:
using BenchmarkTools, LinearAlgebra, Random

Random.seed!(123) # seed
A = randn(1000, 1000);
In [14]:
# if we don't tell Julia it's triangular: O(n^3) complexity
# tril(A) returns a full triangular matrix, same as Matlab
@benchmark eigvals($(tril(A)))
Out[14]:
BenchmarkTools.Trial: 97 samples with 1 evaluation.
 Range (minmax):  49.743 ms55.152 ms   GC (min … max): 0.00% … 0.00%
 Time  (median):     51.251 ms               GC (median):    0.00%
 Time  (mean ± σ):   51.566 ms ±  1.151 ms   GC (mean ± σ):  0.76% ± 1.67%

            ▄▂ ▄█▄ █                   ▂                      
  ▄▆▁▄█▄▆█▄▄██▄███▆█▄█▄█▄▄▁▁▁▆▁█▁▄▁▆▆▄█▄▄▆▆▁▁▄▄▆▄▁▁▁▁▁▁▁▁▁▄ ▁
  49.7 ms         Histogram: frequency by time        54.7 ms <

 Memory estimate: 7.92 MiB, allocs estimate: 13.
In [15]:
# if we tell Julia it's triangular: O(n) complexity
@benchmark eigvals($(LowerTriangular(A)))
Out[15]:
BenchmarkTools.Trial: 10000 samples with 9 evaluations.
 Range (minmax):  2.027 μs933.581 μs   GC (min … max):  0.00% … 99.15%
 Time  (median):     2.528 μs                GC (median):     0.00%
 Time  (mean ± σ):   3.964 μs ±  29.525 μs   GC (mean ± σ):  26.69% ±  3.58%

  ▅▄▁▁▁▇▄▂▃▂   ▂▃                    ▂▂▂▂▁         ▁         ▂
  ███████████▇▅████▆▅▄▁▄▃▇▆▆▅▆▅▃▃▅▆████████▇▇████████▇▇▇▇▇▇ █
  2.03 μs      Histogram: log(frequency) by time      6.79 μs <

 Memory estimate: 7.94 KiB, allocs estimate: 1.
In [16]:
# if we don't tell Julia it's triangular: O(n^3) complexity
# tril(A) returns a full triangular matrix, same as Matlab
@benchmark det($(tril(A)))
Out[16]:
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (minmax):  447.535 μs 1.070 ms   GC (min … max): 0.00% … 0.00%
 Time  (median):     466.876 μs               GC (median):    0.00%
 Time  (mean ± σ):   483.008 μs ± 45.313 μs   GC (mean ± σ):  0.00% ± 0.00%

  ▃  █▇▅▅▃▂▁▁▁▁▁▂▁▁▁▁ ▁▁    ▁                                ▂
  ██▇█████████████████████▇█████▇▇▇▇▇▇▇▇▆▆▇▇▆▆▆▅▄▅▅▅▃▅▅▅▅▅▄▅ █
  448 μs        Histogram: log(frequency) by time       685 μs <

 Memory estimate: 7.94 KiB, allocs estimate: 1.
In [17]:
# if we tell Julia it's triangular: O(n) complexity
@benchmark det($(LowerTriangular(A)))
Out[17]:
BenchmarkTools.Trial: 10000 samples with 9 evaluations.
 Range (minmax):  2.187 μs 1.016 ms   GC (min … max):  0.00% … 99.26%
 Time  (median):     2.732 μs               GC (median):     0.00%
 Time  (mean ± σ):   4.117 μs ± 29.170 μs   GC (mean ± σ):  25.14% ±  3.57%

  ▃▅▁▂▇▅▂▁▂▂                  ▂▂▂▁▁                         ▁
  ███████████▇▅▅▆▅▅▃▅▄▄▄▄▄▆▆▇██████▇██▇▆▆▇▅▆▆▆▆▆▅▅▅▄▅▄▄▂▄▅ █
  2.19 μs      Histogram: log(frequency) by time     8.09 μs <

 Memory estimate: 7.94 KiB, allocs estimate: 1.
In [18]:
@benchmark det($(LowerTriangular(A)))
Out[18]:
BenchmarkTools.Trial: 10000 samples with 9 evaluations.
 Range (minmax):  2.132 μs878.226 μs   GC (min … max):  0.00% … 99.21%
 Time  (median):     2.679 μs                GC (median):     0.00%
 Time  (mean ± σ):   3.790 μs ±  29.033 μs   GC (mean ± σ):  27.44% ±  3.57%

   ▄▅▂   ▃▇▆▃▂▂▁▂▁▁▁▁                                        ▂
  ▇████▇▆█████████████▆▅▄▄▃▃▄▅▅▄▄▅▇▇▇▆▆▆▆▅▁▅▁▄▆▆▆▃▁▄▆▇▇▆▆▆▆ █
  2.13 μs      Histogram: log(frequency) by time      5.57 μs <

 Memory estimate: 7.94 KiB, allocs estimate: 1.