Triangular systems

We consider computer algorithms for solving linear equations $\mathbf{A} \mathbf{x} = \mathbf{b}$, a ubiquitous task in statistics.

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 [1]:
srand(123) # seed
n = 5
A = randn(n, n)
b = randn(n)
# a random matrix
A
Out[1]:
5×5 Array{Float64,2}:
  1.19027   -0.664713   -0.339366   0.368002  -0.979539
  2.04818    0.980968   -0.843878  -0.281133   0.260402
  1.14265   -0.0754831  -0.888936  -0.734886  -0.468489
  0.459416   0.273815    0.327215  -0.71741   -0.880897
 -0.396679  -0.194229    0.592403  -0.77507    0.277726
In [2]:
tril(A) # create another triangular matrix
Out[2]:
5×5 Array{Float64,2}:
  1.19027    0.0         0.0        0.0      0.0     
  2.04818    0.980968    0.0        0.0      0.0     
  1.14265   -0.0754831  -0.888936   0.0      0.0     
  0.459416   0.273815    0.327215  -0.71741  0.0     
 -0.396679  -0.194229    0.592403  -0.77507  0.277726
In [3]:
Al = LowerTriangular(A) # does not create extra matrix
Out[3]:
5×5 LowerTriangular{Float64,Array{Float64,2}}:
  1.19027     ⋅           ⋅          ⋅        ⋅      
  2.04818    0.980968     ⋅          ⋅        ⋅      
  1.14265   -0.0754831  -0.888936    ⋅        ⋅      
  0.459416   0.273815    0.327215  -0.71741   ⋅      
 -0.396679  -0.194229    0.592403  -0.77507  0.277726
In [4]:
fieldnames(Al)
Out[4]:
1-element Array{Symbol,1}:
 :data
In [5]:
Al.data
Out[5]:
5×5 Array{Float64,2}:
  1.19027   -0.664713   -0.339366   0.368002  -0.979539
  2.04818    0.980968   -0.843878  -0.281133   0.260402
  1.14265   -0.0754831  -0.888936  -0.734886  -0.468489
  0.459416   0.273815    0.327215  -0.71741   -0.880897
 -0.396679  -0.194229    0.592403  -0.77507    0.277726
In [6]:
tril(A) \ b # dispatched to LowerTriangular(A) \ b
Out[6]:
5-element Array{Float64,1}:
  1.28031
 -4.48541
  5.32613
  0.44682
 -3.09169
In [7]:
# check source code
@which tril(A) \ b
Out[7]:
\(A::AbstractArray{T,2} where T, B::Union{AbstractArray{T,1}, AbstractArray{T,2}} where T) at linalg/generic.jl:820
In [8]:
LowerTriangular(A) \ b # dispatched to A_ldiv_B
Out[8]:
5-element Array{Float64,1}:
  1.28031
 -4.48541
  5.32613
  0.44682
 -3.09169
In [9]:
# check source code
@which LowerTriangular(A) \ b
Out[9]:
\(A::Union{LowerTriangular, UpperTriangular}, B::AbstractArray{T,1} where T) at linalg/triangular.jl:1624
In [10]:
# or use BLAS wrapper directly
LinAlg.BLAS.trsv('L', 'N', 'N', A, b)
Out[10]:
5-element Array{Float64,1}:
  1.28031
 -4.48541
  5.32613
  0.44682
 -3.09169
In [11]:
?LinAlg.BLAS.trsv
Out[11]:
trsv(ul, tA, dA, A, b)

Returns 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.

In [12]:
using BenchmarkTools

srand(123) # seed
A = randn(1000, 1000)

# if we don't tell Julia it's triangular
@benchmark eigvals(tril(A))
Out[12]:
BenchmarkTools.Trial: 
  memory estimate:  15.55 MiB
  allocs estimate:  22
  --------------
  minimum time:     45.093 ms (0.00% GC)
  median time:      50.140 ms (3.11% GC)
  mean time:        50.127 ms (2.49% GC)
  maximum time:     55.333 ms (0.00% GC)
  --------------
  samples:          100
  evals/sample:     1
In [13]:
# if we tell Julia it's triangular
@benchmark eigvals(LowerTriangular(A))
Out[13]:
BenchmarkTools.Trial: 
  memory estimate:  8.02 KiB
  allocs estimate:  4
  --------------
  minimum time:     2.232 μs (0.00% GC)
  median time:      2.728 μs (0.00% GC)
  mean time:        3.355 μs (10.53% GC)
  maximum time:     206.033 μs (93.15% GC)
  --------------
  samples:          10000
  evals/sample:     9
In [14]:
# if we don't tell Julia it's triangular
@benchmark det(tril(A))
Out[14]:
BenchmarkTools.Trial: 
  memory estimate:  7.64 MiB
  allocs estimate:  7
  --------------
  minimum time:     2.564 ms (0.00% GC)
  median time:      3.212 ms (0.00% GC)
  mean time:        4.040 ms (21.96% GC)
  maximum time:     10.679 ms (31.87% GC)
  --------------
  samples:          1233
  evals/sample:     1
In [15]:
# if we tell Julia it's triangular
@benchmark det(LowerTriangular(A))
Out[15]:
BenchmarkTools.Trial: 
  memory estimate:  8.03 KiB
  allocs estimate:  5
  --------------
  minimum time:     2.589 μs (0.00% GC)
  median time:      3.088 μs (0.00% GC)
  mean time:        3.684 μs (12.47% GC)
  maximum time:     250.764 μs (96.42% GC)
  --------------
  samples:          10000
  evals/sample:     9
In [16]:
versioninfo()
Julia Version 0.6.2
Commit d386e40c17 (2017-12-13 18:08 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin14.5.0)
  CPU: Intel(R) Core(TM) i7-6920HQ CPU @ 2.90GHz
  WORD_SIZE: 64
  BLAS: libopenblas (USE64BITINT DYNAMIC_ARCH NO_AFFINITY Haswell)
  LAPACK: libopenblas64_
  LIBM: libopenlibm
  LLVM: libLLVM-3.9.1 (ORCJIT, skylake)