LU Decomposition and Partial Pivoting
Learning Objectives
- Read LU decomposition as Gaussian elimination with its multipliers recorded, and state when exists.
- Compute by hand with partial pivoting, then solve by forward and back substitution and reuse the factors for a new right-hand side.
- Count the exact number of floating-point operations in the factorization and in each solve.
- Show in float64 how elimination without pivoting destroys the answer on a well-conditioned matrix, and measure forward error, backward error, and growth.
- Separate the partial-pivoting pivot from the simplex pivot, and name the cases where partial pivoting is unnecessary or insufficient.
Context & Motivation
gaussian-elimination solved a system by row operations, with a swap only when a needed pivot was zero. Two things change on a computer. First, that lesson threw its multipliers away, so a second right-hand side would repeat all the work, although the matrix is the same. Keeping the multipliers fixes this: the work on is done once and each new is cheap. Second, in floating point the swap rule is not enough. A pivot that is tiny rather than zero passes the rule and ruins the result, even when the problem itself is well conditioned, in the sense of conditioning-and-numerical-stability.
Core Theory
Elimination records its own inverse
Step subtracts times row from each row . In matrix form that is with , where holds the multipliers below position . After steps is upper triangular. Each inverse just flips a sign, , and in the order they appear the products do not interact:
So is unit lower triangular with the multipliers sitting in place. Elimination needs no second matrix: multipliers overwrite the zeros they create, which is the format scipy.linalg.lu_factor returns.
Existence. A square has with unit lower triangular and nonsingular upper triangular if and only if every leading principal submatrix of is nonsingular. Nonsingularity of alone is not enough: is nonsingular and has no such factorization, since would contradict .
Why a small pivot does damage
Rounding errors in a factorization scale with the entries of and , not with those of . A small pivot makes a large multiplier, which scales a row up, and adding that to a smaller row rounds the smaller row away. Two measurements watch this: the growth factor , and the backward error , which says how much the data must change for to be an exact solution. The relative forward error is at most about the condition number times the backward error, so a large forward error on a well-conditioned matrix indicts the algorithm.
Partial pivoting and PA = LU
At step , search column on and below the diagonal for the largest magnitude, swap that row into position (whole rows, stored multipliers included), then eliminate. Every multiplier now has . "Partial" means only a column is searched; full pivoting searches the whole remaining block.
Theorem. For every square there is a permutation matrix with , where is unit lower triangular with and is upper triangular. is nonsingular if and only if every .
To solve , solve forward and backward. Also for swaps.
This pivot is not the simplex pivot
the-simplex-method-tableau-and-pivoting also says "pivot". The word is shared because each one divides by an entry to clear a column; the jobs are different.
| Partial pivoting in LU | Simplex pivot | |
|---|---|---|
| Purpose | limit rounding error | move to an adjacent vertex |
| Entry chosen by | largest magnitude in the column | entering rule on the objective row, then the ratio test |
| Operation | swap rows, eliminate below only | Gauss-Jordan: clear the column everywhere, objective row included |
| Effect on the answer | none: the same , by a route with less rounding | changes the current solution, on purpose |
| In exact arithmetic | needed only for zero pivots | is the algorithm |
The simplex rule looks at feasibility and the objective, not at entry size, so its pivot can be small. The partial-pivoting rule looks only at size.
Counting the work
Count one flop for each , , , . Step does divisions and multiply-subtract pairs, so
Forward substitution with unit diagonal costs . Back substitution costs , which includes divisions. Each solve is therefore flops. Pivot search adds comparisons and the swaps, none of which are flops. With right-hand sides the total is .
Worked Examples
Example 1: the same system, now factored
Use the system of gaussian-elimination:
That lesson's row operations used multipliers and , and the third row needed nothing in column 2. Keeping them gives, with no swaps,
With partial pivoting, the largest entry of column 1 is the , so rows 1 and 2 swap. The multipliers are , leaving rows and . Column 2 is a tie, so no swap, the multiplier is , and
Now , forward substitution gives , and back substitution gives , the answer of that lesson. A new needs no elimination: , , , for 15 flops. Also . scipy.linalg.lu returns the same and , with its defined by , so its is the transpose of this one. numpy.linalg.solve gives the same two solutions.
Example 2: ordinary entries, a pivot made of noise
Nothing here is small, and , so the problem is well conditioned. In decimal the first step makes the entry exactly , and a swap would follow. But and are not binary fractions. In float64, , so the entry becomes . That is rounding noise, it is nonzero, and the swap rule of gaussian-elimination never fires. The next multiplier is , and , in which the has vanished. The results:
textmethod x max error backward error growth no pivoting (1.3333, 0, 1.0000) 1.0 0.12 2.0e+16 partial pivoting (1, 1, 1) 2.2e-16 0 1 numpy.linalg.solve (1, 1, 1)
The residual of the unpivoted answer is : the first two equations hold and the third was lost. Partial pivoting takes the row with as the second pivot, and the noise entry only produces a multiplier of .
Example 3: the size of the damage, and a scaling trap
For and , the exact solution is close to and for all these :
textepsilon error, no pivoting error, partial pivoting 1e-08 5.0e-09 2.2e-16 1e-12 2.2e-05 0 1e-20 1.0 (x = (0, 1)) 0
Partial pivoting compares raw sizes, so it depends on scaling. Multiply the first equation of the case by : with . The solution is the same, but the first column is now a tie, no swap happens, and both a hand-written LU and numpy.linalg.solve return . Dividing each row by its largest entry first recovers . Partial pivoting assumes rows of comparable scale.
The damage also appears without any planted entry. Take rng = numpy.random.default_rng(42), A = rng.uniform(-1, 1, (1000, 1000)), x = rng.uniform(-1, 1, 1000), b = A @ x, for which . With relative forward error in the infinity norm:
textforward error backward error growth max |L| no pivoting 4.5e-10 7.5e-13 2.9e+04 1.3e+03 partial 1.3e-12 1.3e-15 4.7e+01 1
Here pivoting saves more than two digits, and the implementation agrees with numpy.linalg.solve to .
Example 4: the counts, run
An instrumented loop that increments a counter at every arithmetic operation, on random matrices:
textn sub mul div factor formula forward back solve 2n^2-n 2 1 1 1 3 3 2 4 6 6 3 5 5 3 13 13 6 9 15 15 10 285 285 45 615 615 90 100 190 190 100 328350 328350 4950 661650 661650 9900 10000 19900 19900 200 2646700 2646700 19900 5313300 5313300 39800 40000 79800 79800
For the factorization costs flops and one solve , so one factorization equals about 667 solves. For 100 right-hand sides, factoring once and solving 100 times costs flops, against for 100 separate eliminations, a factor of 87.
This also settles "just invert ". Solving for the columns of adds to the factorization, about in this count (less if the zeros of the unit vectors are exploited), and then costs , exactly what the two triangular solves cost. It is no faster per right-hand side, and on the random matrix of Example 3 its backward error was against for numpy.linalg.solve, 38 times larger; seeds 0 to 3 gave factors from 22 to 52.
Example 5: where partial pivoting is not enough, and where it is not needed
Not enough. The theorem bounds multipliers, not growth, and can be attained. Take ones on the diagonal, below it, and ones in the last column. Every entry below the diagonal ties with the pivot, so no swap happens (ties keep the current row), the last column doubles at each step, and for :
textkappa_2 = 26.8 growth = 5.8e+17 (2^59) numpy.linalg.solve: forward error 0.89, backward error 0.045 QR factorization: forward error 3.0e-15
So partial pivoting is not backward stable in the worst case. It is the default because such matrices are rare in practice: over the 20 matrices numpy.random.default_rng(s).uniform(-1, 1, (500, 500)) with the growth stayed between 25 and 41.
Singular . with rows , , is singular and has no solution. Rounding leaves instead of , so numpy.linalg.solve raises nothing and returns entries of size . scipy.linalg.solve returns the same with an ill-conditioning warning. numpy.linalg.solve does raise LinAlgError on , where the pivot is exactly . A nonzero pivot does not prove nonsingularity.
Not needed. If each diagonal entry strictly dominates its column, it stays the largest, and partial pivoting never swaps. Take rng = numpy.random.default_rng(42), R = rng.uniform(-1, 1, (500, 500)), x = rng.uniform(-1, 1, 500), b = A @ x. With equal to except that each diagonal entry is replaced by plus the sum of the absolute values of the other entries of its column, there were 0 swaps, growth , and no-pivot backward error . The symmetric positive definite , with , gave growth and backward error with no pivoting, and such matrices use Cholesky at about half the flops. iterative-methods-jacobi-and-gauss-seidel takes up the sparse case, where factorization has a different problem.
Common Misconceptions & Pitfalls
- "Pivoting is for zero pivots." In floating point the dangerous pivot is the small one, and in Example 2 a true zero arrived disguised as .
- "A bad answer means a bad matrix." Example 2 has . The algorithm did the damage.
- "Partial pivoting makes LU always stable." Example 5 lost every digit at with . It is reliable in practice, not in the worst case.
- "The simplex pivot is the same pivot." One chooses a vertex and changes the solution. The other only reduces rounding and leaves the solution alone.
- "A small residual means an accurate solution." It means small backward error. The forward error is that times up to the condition number.
- "Invert , then multiply." It costs more up front, the same per right-hand side, and its backward error was 38 times larger on the matrix of Example 4.
- "
scipy.linalg.lugives ." It gives . Check the convention before using its .
Summary
LU decomposition is elimination with memory: the multipliers form a unit lower triangular , the result is , and with nonsingular exists exactly when all leading principal submatrices are nonsingular. Partial pivoting takes the largest entry of each column, which gives for every square matrix with . The factorization costs exactly flops and each solve , so reusing factors is the main practical gain. Without pivoting, a noise pivot of destroyed a system with , and a random system lost more than two digits. The pivot here reduces rounding and leaves the solution unchanged, unlike the simplex pivot. It depends on row scaling, it can be defeated by growth of , and it is unnecessary for diagonally dominant and positive definite matrices.
Documentation Links
- Driscoll and Braun: Fundamentals of Numerical Computation, 2.4 LU factorization
- Driscoll and Braun: Fundamentals of Numerical Computation, 2.5 Efficiency of matrix computations
- Driscoll and Braun: Fundamentals of Numerical Computation, 2.6 Row pivoting
- Driscoll and Braun: Fundamentals of Numerical Computation, 2.9 Exploiting matrix structure
- Goldberg (1991): What Every Computer Scientist Should Know About Floating-Point Arithmetic
- SciPy documentation: scipy.linalg.lu
- SciPy documentation: scipy.linalg.lu_factor
- NumPy documentation: numpy.linalg.solve