#!/usr/bin/env python # coding: utf-8 # ChEn-3170: Computational Methods in Chemical Engineering Fall 2018 UMass Lowell; Prof. V. F. de Almeida **24Sep2018** # # # 05. Computational Linear Algebra Fundamentals # $ # \newcommand{\Amtrx}{\boldsymbol{\mathsf{A}}} # \newcommand{\Bmtrx}{\boldsymbol{\mathsf{B}}} # \newcommand{\Cmtrx}{\boldsymbol{\mathsf{C}}} # \newcommand{\Dmtrx}{\boldsymbol{\mathsf{D}}} # \newcommand{\Mmtrx}{\boldsymbol{\mathsf{M}}} # \newcommand{\Imtrx}{\boldsymbol{\mathsf{I}}} # \newcommand{\Pmtrx}{\boldsymbol{\mathsf{P}}} # \newcommand{\Qmtrx}{\boldsymbol{\mathsf{Q}}} # \newcommand{\Lmtrx}{\boldsymbol{\mathsf{L}}} # \newcommand{\Umtrx}{\boldsymbol{\mathsf{U}}} # \newcommand{\xvec}{\boldsymbol{\mathsf{x}}} # \newcommand{\avec}{\boldsymbol{\mathsf{a}}} # \newcommand{\bvec}{\boldsymbol{\mathsf{b}}} # \newcommand{\cvec}{\boldsymbol{\mathsf{c}}} # \newcommand{\rvec}{\boldsymbol{\mathsf{r}}} # \newcommand{\norm}[1]{\bigl\lVert{#1}\bigr\rVert} # \DeclareMathOperator{\rank}{rank} # $ # --- # ## Table of Contents # * [Theory](#theory) # * [Matrix-vector and matrix-matrix product operations](#product) # * [NumPy and SciPy Linear Algebra](#pylinalg) # + [Matrix solve](#pysolve) # + [$\Lmtrx$ forward solve](#pyl) # + [$\Umtrx$ backward solve](#pyu) # + [$\Amtrx = \Pmtrx\,\Lmtrx\,\Umtrx$ factorization](#[pyplu) # * [Course Linear Algebra](#courselinalg) # + [$\Lmtrx$ forward solve](#l) # + [$\Umtrx$ forward solve](#u) # + [$\Amtrx = \Lmtrx\,\Umtrx$ factorization](#lu) # + [$\Pmtrx\,\Amtrx = \Lmtrx\,\Umtrx$ factorization](#plu) # + [$\Pmtrx\,\Amtrx\,\Qmtrx = \Lmtrx\,\Umtrx$ factorization](#pqlu) # --- # ## Theory # The course notes (OneNote [ChEn-3170-linalg](https://studentuml-my.sharepoint.com/:o:/g/personal/valmor_dealmeida_uml_edu/EhHdq8qIW_JNiMBu60tKHi8BV6Nv8jonllo__NOtX7JgkQ?e=jA5xj8)) cover basic elements of lineary system of algebraic equations. Their representation in row and column formats and the insight gained from them when searching for a solution of the system. # # Some basic theoretical aspects of solving for $\xvec$ in the matrix equation $\Amtrx\,\xvec = \bvec$ are covered. $\Amtrx$ is a matrix, $\bvec$ and $\xvec$ are vectors. # ## Matrix-vector and matrix-matrix product operations # The following operations between vectors and matrices are obtained directly from the buil-in functions in the `numpy` package. # In[ ]: '''Import the NumPy package as usual''' import numpy as np # Scalar product of two vectors: $\avec \cdot \bvec$. # In[ ]: '''Vector scalar product or dot product of vectors''' a_vec = np.array( np.random.random(3) ) b_vec = np.array( np.random.random(3) ) a_vec_dot_b_vec = np.dot( a_vec, b_vec ) # clear linear algebra operation print('a.b =', a_vec_dot_b_vec) a_vec_x_b_vec = a_vec @ b_vec # consistent linear algebra multiplication print('a@b =', a_vec_x_b_vec ) # Matrix vector product: $\Amtrx\,\bvec$. # In[ ]: '''Matrix-vector product''' a_mtrx = np.array( [ [ 2., 1., 1.], # per course notes (p 04) [ 4., -6., 0.], [-2., 7., 2.]]) b_vec = np.array( [5., -2., 9.]) # per course notes a_mtrx_x_b_vec = a_mtrx @ b_vec # linear algebra matrix-vector product print('A x b =', a_mtrx_x_b_vec) # Matrix-vector product: $\Imtrx\,\bvec = \bvec$. Note: $\begin{pmatrix} # 1 & 0 & 0 \\ # 0 & 1 & 0 \\ # 0 & 0 & 1 # \end{pmatrix} \, \begin{pmatrix} b_1\\b_2\\b_3 \end{pmatrix} = \begin{pmatrix} b_1\\b_2\\b_3 \end{pmatrix} $. # In[ ]: '''Identity-matrix vector product''' i_mtrx = np.eye(3) i_mtrx_x_b_vec = i_mtrx @ b_vec # linear algebra matrix-vector product print('I x b =', i_mtrx_x_b_vec) print('b =', b_vec) # Matrix-matrix product: $\Imtrx\,\Amtrx = \Amtrx$. Note: $\begin{pmatrix} # 1 & 0 & 0 \\ # 0 & 1 & 0 \\ # 0 & 0 & 1 # \end{pmatrix} \, # \begin{pmatrix} # A_{1,1} & A_{1,2} & A_{1,3} \\ # A_{2,1} & A_{2,2} & A_{2,3} \\ # A_{3,1} & A_{3,2} & A_{3,3} # \end{pmatrix} = # \begin{pmatrix} # A_{1,1} & A_{1,2} & A_{1,3} \\ # A_{2,1} & A_{2,2} & A_{2,3} \\ # A_{3,1} & A_{3,2} & A_{3,3} # \end{pmatrix} # $. # In[ ]: '''Matrix-matrix product IA = A''' i_mtrx_x_a_mtrx = i_mtrx @ a_mtrx # linear algebra matrix-matrix product print('I x A =\n', i_mtrx_x_a_mtrx) print('A =\n', a_mtrx) # Matrix-matrix product: $\Amtrx\,\Bmtrx = \Cmtrx$. Note: # $\begin{pmatrix} # A_{1,1} & A_{1,2} & A_{1,3} \\ # A_{2,1} & A_{2,2} & A_{2,3} \\ # A_{3,1} & A_{3,2} & A_{3,3} # \end{pmatrix} \, # \begin{pmatrix} # B_{1,1} & B_{1,2} & B_{1,3} \\ # B_{2,1} & B_{2,2} & B_{2,3} \\ # B_{3,1} & B_{3,2} & B_{3,3} # \end{pmatrix} = # \begin{pmatrix} # C_{1,1} & C_{1,2} & C_{1,3} \\ # C_{2,1} & C_{2,2} & C_{2,3} \\ # C_{3,1} & C_{3,2} & C_{3,3} # \end{pmatrix} # $ where each $C_{i,j}$ is a vector product of the $i$th row of $\Amtrx$ and the $j$th column of $\Bmtrx$, *i.e.* # $C_{i,j} = \sum\limits_{k=1}^3 A_{i,k}\, B_{k,j}$. # In[ ]: '''Matrix-matrix product AB = C''' b_mtrx = np.array( [[5. , 5. , 5.], [-2., -2., -2.], [9. , 9. , 9.]] ) c_mtrx = a_mtrx @ b_mtrx # linear algebra matrix-matrix product print('A =\n', a_mtrx) print('B =\n', b_mtrx) print('C =\n', c_mtrx) #
# The matrix-matrix product: $\Bmtrx\,\Amtrx = \Dmtrx \ne \Cmtrx$, does not commute in general. Note # $D_{i,j} = \sum\limits_{k=1}^3 B_{i,k}\, A_{k,j}$. #
# In[ ]: '''Matrix-matrix product BA = D''' b_mtrx = np.array( [[5. , 5. , 5.], [-2., -2., -2.], [9. , 9. , 9.]] ) d_mtrx = b_mtrx @ a_mtrx # linear algebra matrix-matrix product print('A =\n', a_mtrx) print('B =\n', b_mtrx) print('D =\n', d_mtrx) # ## NumPy and SciPy Linear Algebra # [NumPy](http://www.numpy.org/) has extensive support for [linear algebra](https://docs.scipy.org/doc/numpy/reference/routines.linalg.html?highlight=linear%20algebra) arrays. We collect here the relevant operations for this course. # However additional resources are instead added to [SciPy](https://docs.scipy.org/doc/scipy-1.1.0/reference/) for general scientific computing including [linear algebra](https://docs.scipy.org/doc/scipy-1.1.0/reference/tutorial/linalg.html). # # Linear algebra operations are obtained from the `linalg` sub-package of the `numpy` package, and the `linalg` sub-package of `scipy`. # In[ ]: '''Import the NumPy linear algebra sub-package as usual''' #import numpy.linalg as linalg #from numpy import linalg # often used alternative '''or leave it commented since the usage of np.linalg is self-documenting''' # The 2-norm or norm (magnitude) of a vector $\bvec$ is indicated as $\norm{\bvec}$ and computed as follows: # In[ ]: '''Vector norm (or magnitude)''' norm_b_vec = np.linalg.norm( b_vec ) # default norm is the 2-norm print('||b|| =', norm_b_vec) # same as magnitude # ### Matrix solve # # *Solve* for $\xvec$ in the matrix equation $\Amtrx\,\xvec = \bvec$, where $\Amtrx = # \begin{pmatrix} # 2 & 1 & 1 \\ # 4 & -6 & 0 \\ # -2 & 7 & 2 # \end{pmatrix} # $ # and $\bvec = \begin{pmatrix} 5\\ -2\\ 9 \end{pmatrix}$. # In[ ]: '''Matrix solver (this is short for solution of a linear algebraic system of equations)''' x_vec = np.linalg.solve( a_mtrx, b_vec ) # solve linear system for A, b; per course notes 04 print('solution x =', x_vec) # The residual vector defined as $\rvec = \bvec - \Amtrx\,\xvec$ is of importance. So is its norm $\norm{\rvec}$. # In[ ]: '''Verify the accuracy of the solution''' res_vec = b_vec - a_mtrx @ x_vec print('b - A x =',res_vec) print('||b - A x|| =',np.linalg.norm( res_vec )) # The rank of a matrix of coefficients, $\rank(\Amtrx)$, of a linear algebraic system of equations determines weather the solution is unique or singular. # In[ ]: '''Matrix rank''' k = np.linalg.matrix_rank( a_mtrx ) # rank; per course notes 14 print('rank(A) =',k) print('shape(A) =',a_mtrx.shape) if k == a_mtrx.shape[0] and k == a_mtrx.shape[1]: # flow control print('A is non-singular; solution is unique ') else: print('A is singular') # Why is this matrix $\Bmtrx$ singular? # In[ ]: b_mtrx = np.array( [ [ 2., 1., 3.], # singular [ 4., -6., -2.], [-2., 7., 5.]]) k = np.linalg.matrix_rank( b_mtrx ) # rank print('rank(B) =',k) print('shape(B) =',b_mtrx.shape) if k == b_mtrx.shape[0] and k == b_mtrx.shape[1]: # flow control print('B is non-singular; solution is unique ') else: print('B is singular') # In[ ]: '''Matrix determinant''' det_a_mtrx = np.linalg.det( a_mtrx ) # determinant; course notes 16 print('det(A) =', det_a_mtrx) # The inverse matrix is denoted as $\Amtrx^{-1}$ and is computed as the matrix that multiplies $\bvec$ and produces the solution $\xvec$, that is, $\xvec = \Amtrx^{-1}\,\bvec$. # In[ ]: '''Matrix inverse''' a_mtrx_inv = np.linalg.inv( a_mtrx ) # matrix inverse; per course notes 17 print('A^-1 =\n', a_mtrx_inv) # Recall $\Amtrx^{-1}\,\Amtrx = \Imtrx$ where $\Imtrx$ is the identity matrix. # In[ ]: '''Identity matrix''' i_mtrx = a_mtrx_inv @ a_mtrx # identity matrix; per course notes 17 print('A^-1 A =\n',i_mtrx) # Using the inverse, the same solution will be found: $\xvec = \Amtrx^{-1}\,\bvec$. # In[ ]: '''Solution using the inverse''' x_vec_again = a_mtrx_inv @ b_vec # matrix-vector multiply; per course notes 17 print('solution x =', x_vec_again) # This is the element-by-element reciprocal of the matrix $(\Amtrx)^{-1}$, which is very different than the inverse. # In[ ]: '''Inverse power of a matrix''' #a_mtrx_to_negative_1 = a_mtrx**(-1) # this will cause an error (division by zero) # Let's look at the determinant of a larger matrix, say $\Mmtrx$. # In[ ]: '''Generate a larger matrix from an image''' from matplotlib import pyplot as plt # import the pyplot function of the matplotlib package plt.rcParams['figure.figsize'] = [20, 4] # extend the figure size on screen output mtrx = plt.imread('https://raw.githubusercontent.com/dpploy/chen-3170/master/notebooks/images/cermet.png',format='png') m_mtrx = mtrx[:,:] plt.figure(1) # create a figure placeholder plt.imshow( m_mtrx,cmap='gray') plt.title('Cermet',fontsize=14) plt.xlabel('x pixels',fontsize=12) plt.ylabel('y pixels',fontsize=12) print('M shape =', m_mtrx.shape) # In[ ]: '''Larger matrix determinant''' det_m_mtrx = np.linalg.det( m_mtrx ) # determinant print('max(M) =',m_mtrx.max()) print('min(M) =',m_mtrx.min()) print('det(M) = %10.3e (not an insightful number)'%det_m_mtrx) # formatting numeric output print('rank(M) = ',np.linalg.matrix_rank( m_mtrx, tol=1e-5 ) ) # Let's *solve* for this matrix with $\cvec$ as the right side vector, that is, $\Mmtrx\,\xvec = \cvec$. # In[ ]: '''Solve M x = c and plot x''' c_vec = np.random.random(mtrx.shape[0]) # any c will do it sol = np.linalg.solve( m_mtrx, c_vec ) # solve linear system for A, b plt.figure(2) plt.plot(range(c_vec.size),sol,'k') plt.title('M x = c',fontsize=20) plt.xlabel('n',fontsize=18) plt.ylabel('$c_j$',fontsize=18) print('') # In[ ]: res_vec = c_vec - m_mtrx @ sol #print('c - M x =',res_vec) print('||c - M x|| =%12.4e'%np.linalg.norm( res_vec )) # ### $\Lmtrx$ forward solve # A lower triangular matrix allows for a forward solve. # In[ ]: '''L forward solve''' l_mtrx = np.array( [[1., 0., 0.], # per course notes [2., 3., 0.], [4., 5., 6.]] ) b_vec = np.array( [1.,2.,3.] ) x_vec = np.linalg.solve( l_mtrx, b_vec ) np.set_printoptions(precision=3) # one way to control printing of numpy arrays print('x = ',x_vec) # ### $\Umtrx$ backward solve # An upper triangular matrix allows for a backward solve. # In[ ]: '''U backward solve''' u_mtrx = np.array( [[1., 2., 3.], # per course notes [0, 4., 5.], [0., 0., 6.]] ) b_vec = np.array( [1.,2.,3.] ) x_vec = np.linalg.solve( u_mtrx, b_vec ) np.set_printoptions(precision=3) # one way to control printing of numpy arrays print('x = ',x_vec) # ### $\Pmtrx\,\Lmtrx\,\Umtrx$ factorization # The factors: $\Pmtrx$, $\Lmtrx$, and $\Umtrx$ where $\Pmtrx\,\Lmtrx\,\Umtrx = \Amtrx$ can be obtained from the SciPy linear algebra package. $\Pmtrx$ is a permutation matrix if the underlying Gaussian elimination used to construct the $\Lmtrx$ and $\Umtrx$ factors. # In[ ]: '''Import only the linear algebra package''' import scipy.linalg import numpy as np # In[ ]: '''P L U factors of A''' a_mtrx = np.array( [[1, 2, 3], [4, 5, 6], [7, 8, 10]] ) (p_mtrx, l_mtrx, u_mtrx) = scipy.linalg.lu( a_mtrx ) print('P =\n',p_mtrx) print('L =\n',l_mtrx) print('U =\n',u_mtrx) print('Checking...') print('PLU - A =\n', p_mtrx @ l_mtrx @ u_mtrx - a_mtrx) # In[ ]: '''P^-1 = P^T (i.e. the transpose of a permutation matrix is its inverse)''' pinv_mtrx = np.linalg.inv(p_mtrx) print('P^-1 =\n', pinv_mtrx) print('Checking...') print('P^-1 - P^T =\n', pinv_mtrx - p_mtrx.transpose()) # In[ ]: '''PLU x = b; that is: Forward: L y = P^-1 b, Backward: U x = y ''' b_vec = np.array([1.,2.,3.]) y_vec = scipy.linalg.solve(l_mtrx, p_mtrx.transpose() @ b_vec) # L y = P^T b x_vec = scipy.linalg.solve(u_mtrx, y_vec) # U x = y print('x =', x_vec) x_vec_gold = scipy.linalg.solve( a_mtrx, b_vec ) # solution using A x = b print('||x - x_gold|| =',scipy.linalg.norm(x_vec-x_vec_gold)) # In[ ]: '''Deterninant of U or L: product of the diagonal''' det_u = np.linalg.det(u_mtrx) print('det(U) = %8.3e'%det_u) diag_vec = np.diagonal(u_mtrx) prod = np.prod(diag_vec) print('diag(U) product = %8.3e'%prod ) # In[ ]: '''Determinant of P (always +1 or -1)''' det_p = np.linalg.det(p_mtrx) print('det(P) = %8.3e'%det_p) # In[ ]: '''Determinant of A = det(PLU)''' det_l = np.prod( np.diagonal(l_mtrx) ) det_plu = det_p * det_l * det_u # last term is det of L print('det(PLU) = %8.3e'%det_plu) print('det(A) = %8.3e'%np.linalg.det(a_mtrx)) # ## ChEn-3170 Linear Algebra # In this course various algorithms need to be programmed. These should be compared to `SciPy` and/or `NumPy`. # ### $\Lmtrx$ forward solve # A lower triangular matrix allows for a forward solve. # The algorithm for $\Lmtrx\,\xvec=\bvec$ is as follows: # # \begin{equation*} # x_i = \Bigl(b_i - \sum\limits_{j=1}^{i-1} L_{i,j}\,x_j \Bigr)\,L^{-1}_{i,i} \quad\ \forall \quad\ i=1,\ldots,m # \end{equation*} # # for $i$ and $j$ with offset 1. Recall that `NumPy` and `Python` have offset 0 for their sequence data types. # In[ ]: '''L forward solve''' l_mtrx = np.array( [[1., 0., 0.], # per course notes [2., 3., 0.], [4., 5., 6.]] ) b_vec = np.array( [1.,2.,3.] ) from chen_3170.help import forward_solve x_vec = forward_solve( l_mtrx, b_vec ) np.set_printoptions(precision=3) # one way to control printing of numpy arrays print('x = ',x_vec) # ### $\Umtrx$ backward solve # A upper triangular matrix allows for a backward solve. # The algorithm for $\Umtrx\,\xvec=\bvec$ is as follows: # # \begin{equation*} x_i = \Bigl(b_i - \sum\limits_{j=i+1}^{m} U_{i,j}\,x_j \Bigr)\,U^{-1}_{i,i} \quad\ \forall \quad\ i=m,\ldots,1 # \end{equation*} # # for $i$ and $j$ with offset 1. Recall that `NumPy` and `Python` have offset 0 for their sequence data types. # In[ ]: '''U backward solve''' u_mtrx = np.array( [[1., 2., 3.], # per course notes [0, 4., 5.], [0., 0., 6.]] ) b_vec = np.array( [1.,2.,3.] ) try: from chen_3170.toolkit import backward_solve except ModuleNotFoundError: assert False, 'You need to provide your own backward_solve function here. Bailing out.' x_vec = backward_solve( u_mtrx, b_vec ) np.set_printoptions(precision=3) # one way to control printing of numpy arrays print('x = ',x_vec) # ### $\Amtrx = \Lmtrx\,\Umtrx$ factorization # $\Lmtrx\,\Umtrx$ factorization algorithm (without using pivoting) for a square matrix $\overset{(m \times m)}{\Amtrx}$ computes the $\Lmtrx\,\Umtrx$ factors. The factorization is obtained by elimination steps $k = 1,\ldots,m-1$ so that # # \begin{equation*} # A^{(k+1)}_{i,j} = A^{(k)}_{i,j} - A^{(k)}_{k,j}\, m_{i,k} \quad\ \forall\ i=k+1,\ldots,m \quad\ \text{and} \quad\ j=k,\ldots,m # \end{equation*} # # where the multipliers $m_{i,k}$ are given by $m_{i,k} = \frac{A^{(k)}_{i,k}}{A^{(k)}_{k,k}}$. When $k = m-1$, $A^{(m)}_{i,j}$, is upper triangular, that is, $U_{i,j} = A^{(m)}_{i,j}$ . The lower triangular matrix is obtained using the multipliers $m_{i,k}$, that is $L_{i,j} = m_{i,j} \ \forall \ i>j$, $L_{i,i}=1$, and $L_{i,j}=0 \ \forall \ i factorization # The factors: $\Pmtrx$, $\Lmtrx$, and $\Umtrx$ where $\Lmtrx\,\Umtrx = \Pmtrx\,\Amtrx$ can be obtained from **partial pivoting** strategy to the $\Lmtrx\,\Umtrx$ factorization algorithm shown above. $\Pmtrx$ is a row permutation matrix obtained by the underlying Gaussian elimination used to construct the $\Lmtrx$ and $\Umtrx$ factors. # # Program a $\Lmtrx\,\Umtrx$ factorization algorithm (using partial pivoting) for a square matrix $\overset{(m \times m)}{\Amtrx}$ and compute the $\Pmtrx\,\Lmtrx\,\Umtrx$ factors. # # The factorization is obtained by elimination steps $k = 1,\ldots,m-1$ so that $A^{(k+1)}_{i,j} = A^{(k)}_{i,j} - A^{(k)}_{k,j}\, m_{i,k} \ \forall\ i=k+1,\ldots,m \ \text{and}\ j=k,\ldots,m$ where the multipliers $m_{i,k}$ are given by $m_{i,k} = \frac{A^{(k)}_{i,k}}{A^{(k)}_{k,k}}$. When $k = m-1$, $A^{(m)}_{i,j}$, is upper triangular, that is, $U_{i,j} = A^{(m)}_{i,j}$ . The lower triangular matrix is obtained using the multipliers $m_{i,k}$, that is $L_{i,j} = m_{i,j} \ \forall \ i>j$, $L_{i,i}=1$, and $L_{i,j}=0 \ \forall \ i factorization # The factors: $\Pmtrx$, $\Qmtrx$, $\Lmtrx$, and $\Umtrx$ where $\Lmtrx\,\Umtrx = \Pmtrx\,\Amtrx\,\Qmtrx$ can be obtained from a user-developed algorithm with **complete pivoting**. $\Pmtrx$ is a row permutation matrix and $\Qmtrx$ is a column permutation matrix, obtained by the underlying Gaussian elimination used to construct the $\Lmtrx$ and $\Umtrx$ factors. # # Program a $\Lmtrx\,\Umtrx$ factorization algorithm (using complete pivoting) for a square matrix $\overset{(m \times m)}{\Amtrx}$ and compute the $\Pmtrx\,\Qmtrx\,\Lmtrx\,\Umtrx$ factors. The factorization is obtained by elimination steps $k = 1,\ldots,m-1$ so that $A^{(k+1)}_{i,j} = A^{(k)}_{i,j} - A^{(k)}_{k,j}\, m_{i,k} \ \forall\ i=k+1,\ldots,m \ \text{and}\ j=k,\ldots,m$ where the multipliers $m_{i,k}$ are given by $m_{i,k} = \frac{A^{(k)}_{i,k}}{A^{(k)}_{k,k}}$. When $k = m-1$, $A^{(m)}_{i,j}$, is upper triangular, that is, $U_{i,j} = A^{(m)}_{i,j}$ . The lower triangular matrix is obtained using the multipliers $m_{i,k}$, that is $L_{i,j} = m_{i,j} \ \forall \ i>j$, $L_{i,i}=1$, and $L_{i,j}=0 \ \forall \ i