Definitions and notation in this review follow Section 2.2 Vectors and Matrices in Biegler (2010), with the invertibility discussion cross-checked against Lay, Linear Algebra and Its Applications, 3rd ed.
Recommended Links
SciPy Lecture Notes (Especially 1.1 - 1.6 for everyone and 2.1 - 2.2 for advanced users)
Tip: We will mostly use SciPy for linear algebra. (It has more sophisticated capabilities than NumPy.) We will use NumPy if/when SciPy does not offer a specific command.
This guide supplies definitions, proof details, and extended calculations for the lecture. Use the sections on spaces and rank, worked factorizations, SVD, and conditioning alongside the shorter handout. Numerical Errors computes roundoff and error measurements. The scaling example returns scaled variables to their original units.
# Load required Python libraries.
import numpy as np
from scipy import linalg
# Seed the random number generator so this notebook is reproducible
# (Pyomo style guide, section 8).
np.random.seed(0)Notation and linear maps¶
Scalars and scalar components are plain, vectors are bold lower case, and matrices are bold upper case: , , , , and . The matrix represents the linear map , . Linearity means
for every and . The columns of are the images of the coordinate vectors: .
Matrix operations¶
For , has entries
The inner dimensions must match. Multiplication composes maps: first apply , then . If is and is , is ; is undefined because its inner dimensions are 7 and 4.
The transpose interchanges rows and columns, . A square matrix is symmetric when and diagonal when for . The identity has and all off-diagonal entries zero.

For the illustrated , the area changes by . A negative determinant reverses orientation; a zero determinant collapses area to zero.
Video: 3Blue1Brown, Linear transformations and matrices and Matrix multiplication as composition.
# Generate random matrices
n = 2
m = 3
Ao = np.random.rand(n, m)
Bo = np.random.rand(m, n)
print("Ao = ")
print(Ao)
print("\nBo = ")
print(Bo)
# What is the dimension of A*B? Try to answer this without the computer.
# Calculate A*B using Python
print("\nAo*Bo =")
print(Ao.dot(Bo))Ao =
[[0.5488135 0.71518937 0.60276338]
[0.54488318 0.4236548 0.64589411]]
Bo =
[[0.43758721 0.891773 ]
[0.96366276 0.38344152]
[0.79172504 0.52889492]]
Ao*Bo =
[[1.40657799 1.08244885]
[1.15806481 0.98996907]]
Activity
Calculate
Create a square matrix and check if it is symmetric
Create a 3x3 identity matrix
# Transpose
print("transpose(Ao) = \n", Ao.transpose(), "\n")
# Create a square matrix. Is it symmetric?
# Identify matrix
print("I = \n", np.identity(3), "\n")transpose(Ao) =
[[0.5488135 0.54488318]
[0.71518937 0.4236548 ]
[0.60276338 0.64589411]]
I =
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
Determinant¶
For a square matrix , expansion along any row gives
where is the entry in row and column , and is the determinant of the matrix obtained by deleting row and column from . Useful properties are
The exponent in the scaling law matters: multiplying the whole matrix by scales each of its rows. A square matrix is singular exactly when .

# Generate random matrices
nd = 2
Ad = np.random.rand(nd, nd)
Bd = np.random.rand(nd, nd)
Cd = np.array([(1, 0, 0), (-2, 0, 0), (0, 1, 1)])
print("Ad = ")
print(Ad)
print("\nBd = ")
print(Bd)
print("\nCd = ")
print(Cd)Ad =
[[0.56804456 0.92559664]
[0.07103606 0.0871293 ]]
Bd =
[[0.0202184 0.83261985]
[0.77815675 0.87001215]]
Cd =
[[ 1 0 0]
[-2 0 0]
[ 0 1 1]]
Activity
Verify the properties listed under Some properties for the determinant include
Are , or singular?
# Property 1
print("det(A*B) = ", linalg.det(Ad.dot(Bd)))
print("det(A)*det(B) = ", linalg.det(Ad) * linalg.det(Bd))det(A*B) = 0.010247347495272154
det(A)*det(B) = 0.010247347495272162
# Property 2# Property 3# Property 4Span, independence, and rank¶
A linear combination of is , with real coefficients. Their span is the set of all such combinations. They are linearly independent if implies for every ; otherwise they are dependent.
A subspace contains and is closed under linear combinations: for all and all real . A basis of is a linearly independent spanning set. Its number of vectors is . The zero subspace has the empty basis and dimension zero.

For , the range or column space is
The null space is . Both are subspaces, but they belong to different spaces: range in , null space in .
The rank is the dimension of the column space, equivalently the row space or the size of the largest square submatrix with nonzero determinant. Full column rank means ; full row rank means . Full rank means ; otherwise the matrix is rank deficient.
Rank-nullity: complete a basis¶
Let be a basis of the null space, and extend it to a basis of the domain. Every is a linear combination of , because the first vectors map to zero. These remaining images are independent: any combination mapping to zero would be both a combination of the last basis vectors and a member of the span of the first . Independence of the whole basis forces every coefficient to vanish. Thus

For the illustrated matrix , the range is and the null space is . Both have dimension one. If solves , every solution is with . If is outside the range, there is no solution.
Video: 3Blue1Brown, Linear combinations, span, and basis vectors, and Inverse matrices, column space, and null space.
Activity. Predict the ranks of , , , , and . Compare with np.linalg.matrix_rank and explain any rank deficiency in terms of dependent columns.
print("rank(A_o) = ", np.linalg.matrix_rank(Ao), "\n")
# Fill in remainder here.rank(A_o) = 2
Inverse¶
A square matrix is invertible when there is a matrix satisfying . Then has the unique solution , although numerical software should solve the system directly rather than form the inverse. Also, . An orthogonal matrix satisfies .
Activity
Calculate the inverse of the square matrix and verify that . (The rectangular matrix has no two-sided inverse.)
Verify that
Is an orthogonal matrix?

If and an inverse existed, multiplicativity of the determinant would give , a contradiction. The Invertible Matrix Theorem collects the converse and equivalent conditions.
For dense unstructured matrices, factorizing and solving usually takes less work than explicitly forming an inverse and multiplying. Leading operation counts for a single right-hand side are approximately for elimination versus for an inverse. Accuracy also depends on the algorithm and data; avoid treating a computed inverse as a universal cure for a difficult solve. See Higham (2002), §14.1, p.260.
A sparse matrix can have a dense inverse, increasing storage as well as arithmetic. For an explicit example, let have ones immediately above the diagonal and zeros elsewhere. Then and
Multiplication telescopes to . The original matrix has nonzeros, while its inverse has nonzeros. This example explains the storage risk; sparse factorization can also create fill and needs an appropriate ordering.
# Task 1
# Task 2
# Task 3
# Is this an orthogonal matrix? Yes, no, or need more information?
theta = 0.2
Q = np.array([(np.cos(theta), -np.sin(theta)), (np.sin(theta), np.cos(theta))])
print("Q = ")
print(Q)Q =
[[ 0.98006658 -0.19866933]
[ 0.19866933 0.98006658]]
Solving Linear Systems¶
Consider the linear system
Al = np.array([(4, 3), (6, 3)])
bl = np.array([1, 0])
print("Al = \n", Al)
print("\nbl = \n", bl)Al =
[[4 3]
[6 3]]
bl =
[1 0]
Explicit Inverse¶
Calculate by explicitly using . Hint: Use linalg.inv().
Ainv = linalg.inv(Al)
print("Ainv = \n", Ainv, "\n")
# Now calculate and print xAinv =
[[-0.5 0.5 ]
[ 1. -0.66666667]]
LU Decomposition¶
Perform LU decomposition on . What structures do the , , and matrices have?

SciPy returns , so the solve is followed by . A permutation is orthogonal: . It need not be symmetric. Partial pivoting interchanges rows to use a largest-magnitude entry in the active column as the pivot. This avoids division by a smaller available pivot; it does not guarantee small element growth for every matrix.
Reuse the factors¶
For a dense system, factorization costs approximately operations, and the two triangular solves cost approximately per right-hand side. With right-hand sides, refactorizing each time costs ; reusing the factors costs . For and , these are and , a ratio of about 125.4. These leading arithmetic counts omit lower-order work and do not predict an exact wall-clock speedup.
Doolittle elimination chooses unit diagonal entries in . See Higham (2002), §9.2, pp.162–163, and §9.4, pp.166–167 for growth and backward error. The worked examples show the handout’s small LU system without the random matrices used below.
# Create test linear systems
# The transpose is deliberate: it gives a permutation matrix P that is NOT
# symmetric, so the MATLAB and SciPy conventions below give different answers.
A = np.random.rand(4, 4).T
print("A = \n", A, "\n")
b = np.random.rand(4, 1)
print("b = \n", b, "\n")A =
[[0.97861834 0.11827443 0.52184832 0.45615033]
[0.79915856 0.63992102 0.41466194 0.56843395]
[0.46147936 0.14335329 0.26455561 0.0187898 ]
[0.78052918 0.94466892 0.77423369 0.6176355 ]]
b =
[[0.61209572]
[0.616934 ]
[0.94374808]
[0.6818203 ]]
# Perform LU decomposition
P, L, U = linalg.lu(A)
# Permutation matrix
print("P = \n", P)
# Lower triangular matrix
print("L = \n", L)
# Upper triangular matrix
print("U = \n", U)
# Verify result
print("P*L*U = \n", P.dot(L.dot(U)), "\n")P =
[[1. 0. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]
[0. 1. 0. 0.]]
L =
[[1. 0. 0. 0. ]
[0.79758282 1. 0. 0. ]
[0.81661924 0.63896662 1. 0. ]
[0.47156214 0.10299414 0.07659477 1. ]]
U =
[[ 0.97861834 0.11827443 0.52184832 0.45615033]
[ 0. 0.85033527 0.35801643 0.25381783]
[ 0. 0. -0.24024999 0.03375169]
[ 0. 0. 0. -0.22504038]]
P*L*U =
[[0.97861834 0.11827443 0.52184832 0.45615033]
[0.79915856 0.63992102 0.41466194 0.56843395]
[0.46147936 0.14335329 0.26455561 0.0187898 ]
[0.78052918 0.94466892 0.77423369 0.6176355 ]]
Is P orthogonal?¶
print("inv(P) = \n", linalg.inv(P), "\n")
print("transpose(P) = \n", P.T, "\n")
print("P.T*P = \n", np.matmul(P.T, P), "\n")inv(P) =
[[ 1. 0. -0. 0.]
[ 0. 0. -0. 1.]
[ 0. 1. -0. 0.]
[ 0. 0. 1. 0.]]
transpose(P) =
[[1. 0. 0. 0.]
[0. 0. 0. 1.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]]
P.T*P =
[[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]
Yes! We see that and .
MATLAB¶
Defines LU decomposition as follows:
Consider
Using MATLAB LU definition
Substitute into linear system:
Let and substitute.
Step 1. Solve for
Step 2. Solve for
The next cell deliberately applies to the SciPy factors computed above. This mixes the two conventions and generally fails the residual check. MATLAB’s own permutation is the transpose of SciPy’s for the same factors. Use in the SciPy solve that follows.
Pb = P.dot(b)
print("P*b = ", Pb, "\n")
yLU = linalg.solve(L, Pb)
print("yLU = ", yLU, "\n")
xLU = linalg.solve(U, yLU)
print("xLU = ", xLU, "\n")
# Check: does this x actually solve A*x = b?
print("||A*x - b|| = ", np.linalg.norm(A.dot(xLU) - b), "\n")P*b = [[0.61209572]
[0.94374808]
[0.6818203 ]
[0.616934 ]]
yLU = [[ 0.61209572]
[ 0.45555105]
[-0.10911075]
[ 0.28973105]]
xLU = [[ 0.98256068]
[ 0.80496658]
[ 0.27328499]
[-1.28746252]]
||A*x - b|| = 0.42382052546128374
SciPy¶
Defines LU decomposition as:
Consider
SciPy LU definition
Substitute into linear system:
Let and substitute.
Step 1. Solve for
Step 2. Solve for
## LU decomposition algorithm.
Pb = (P.T).dot(b)
print("P.T*b = ", Pb, "\n")
yLU = linalg.solve(L, Pb)
print("yLU = ", yLU, "\n")
xLU = linalg.solve(U, yLU)
print("xLU = ", xLU, "\n")
# Check: does this x actually solve A*x = b?
print("||A*x - b|| = ", np.linalg.norm(A.dot(xLU) - b), "\n")P.T*b = [[0.61209572]
[0.6818203 ]
[0.616934 ]
[0.94374808]]
yLU = [[ 0.61209572]
[ 0.19362327]
[-0.00663395]
[ 0.63567297]]
xLU = [[ 1.99078818]
[ 1.22630486]
[-0.3692182 ]
[-2.82470629]]
||A*x - b|| = 3.6821932062951477e-16
What is the difference? A transpose.
Verify our answer with linalg.solve¶
Solve the linear system using linalg.solve
x = linalg.solve(Al, bl)
print("x = \n", x)x =
[-0.5 1. ]
Invertible Matrix Theorem¶
For a square matrix , the following statements are equivalent:
is invertible; its row-reduced form is ; and it has pivots.
has only the trivial solution, so and .
The columns of are linearly independent, form a basis of , and span ; equivalently .
The map is both one-to-one and onto; therefore has a solution for every .
A left or right inverse exists; is invertible; ; and zero is not an eigenvalue.
The row space is , , , and has nonzero singular values.
These equivalences consolidate Lay’s Invertible Matrix Theorem into one searchable list.
Eigenvectors and Eigenvalues¶
For square , a nonzero vector is an eigenvector with eigenvalue when . Thus is singular and eigenvalues are roots of . Counting algebraic multiplicity, .
If is real and symmetric, the spectral theorem gives with real eigenvalues and orthonormal eigenvectors. Only under this symmetry assumption can definiteness be read directly from eigenvalue signs: all positive means positive definite, all nonnegative means positive semidefinite, and mixed signs mean indefinite.
A nonsymmetric real matrix may have complex eigenvalues, and its eigenvalues do not in general classify the quadratic form . That form is governed by the symmetric part . For rectangular matrices, singular values —the square roots of the eigenvalues of —play the analogous role.

A nonsymmetric worked example¶
For ,
For , the equations in are multiples of . The matrix has rank one, and eigenvectors are with . For the eigenvectors are with . These eigendirections are not orthogonal; symmetry is the hypothesis that guarantees an orthonormal eigenbasis. Counting algebraic multiplicity, and . For nonsingular , the eigenpairs of are .
Video: 3Blue1Brown, Eigenvectors and eigenvalues.
Definiteness and the spectral theorem¶
For real symmetric and every nonzero real , positive definite means ; positive semidefinite means . Reverse the signs for negative definite or negative semidefinite. Indefinite means the form takes both positive and negative values. Singular means the matrix is not invertible; it is a different property from definiteness.
Write with orthogonal and set . Then

This proves the eigenvalue sign tests. Choosing proves necessity; summing nonnegative squared coordinates proves sufficiency. A symmetric semidefinite matrix can be singular: has eigenpairs and . Its quadratic form is , so it is positive semidefinite and has a nontrivial null space.
For symmetric nonsingular ,
Multiplying the two factorizations gives . Thus inversion preserves each eigenvector and reciprocates its eigenvalue. Keep each eigenvalue paired with its direction when reading a quadratic form or its inverse.
Level sets of a quadratic form, as one picture¶

Eigenvalues and eigenvectors of a symmetric matrix have a picture: they are the principal axes of the level sets of . In the rotated coordinates the level set is , so the semi-axis along has length --- a large eigenvalue means steep, hence short.
The right-hand panel is the same picture with a condition number of 100. Here is the condition number of a symmetric positive definite matrix (defined properly in the Condition Number section below). Note the axis ratio is , not . The matrices in Four Factorizations, Four Failures below have from about 108 to 1018, and for those the ellipse is not drawable.
This figure is rendered from figures/plots/quadratic-form-level-sets.py and is the same
image printed in the course pack, so what you annotate in lecture and what you see here
cannot drift apart. The cells below are the live version.
Ae = np.array([(0, -4, -6), (-1, 0, -3), (1, 2, 5)])
print("Ae = \n", Ae)Ae =
[[ 0 -4 -6]
[-1 0 -3]
[ 1 2 5]]
Activity
Do you expect this matrix to have negative eigenvalues? Choose: Yes / No / Cannot tell from inspection.
Calculate the eigenvalues.
The eigenvalues of this nonsymmetric matrix are positive. Does that prove for every nonzero ?
Form the symmetric part and use its eigenvalues to classify the quadratic form.
Calculate using only the eigenvalues of .
# Expect negative eigenvalues?
# Yes / No / Cannot tell from inspection?
# Write a sentence to justify your answer.
### Calculate eigenvalues
# Matrix Ae is nonsymmetric
print("Matrix = \n", Ae, "\n")
l, v = linalg.eig(Ae)
print("Eigenvalues = ", l, "\n")
print("Eigenvectors = \n", v, "\n")
# The symmetric part, not Ae's eigenvalues, classifies x.T @ Ae @ x
H = 0.5 * (Ae + Ae.T)
print("Eigenvalues of symmetric part = ", linalg.eigvalsh(H))Matrix =
[[ 0 -4 -6]
[-1 0 -3]
[ 1 2 5]]
Eigenvalues = [1.+0.j 2.+0.j 2.+0.j]
Eigenvectors =
[[-0.81649658 -0.95681941 -0.06734698]
[-0.40824829 0.04849614 -0.81965465]
[ 0.40824829 0.28660904 0.56888543]]
Eigenvalues of symmetric part = [-3.08257569 2. 6.08257569]
Singular value decomposition¶
For with , a thin SVD is
where , , and with . The columns of and are left and right singular vectors. For a rectangular thin , is a projection, not the identity on . For , apply the construction to or use a full SVD with rectangular .

Orthogonal factors preserve 2-norm lengths. Along , ; a zero singular value sends that direction to zero. The number of positive singular values is the exact rank. Numerical rank requires a scale-dependent tolerance: compare each singular value with a tolerance relative to , taking data accuracy into account.
Connection with a symmetric eigenproblem¶
Substitute the factorization and use :
Conversely, diagonalize the positive semidefinite and set for each positive . Then
Complete these left singular vectors to orthonormal columns if the rank is less than ; never divide by a zero singular value. The remaining right singular vectors lie in the null space. For full column rank, the compact formula is .
This is an algebraic connection, not a recommendation to compute an SVD by forming . For full column rank, its 2-norm condition number is , which can lose information about small singular values. Use scipy.linalg.svd directly. It returns Vh, meaning for real data; full_matrices=False requests the thin factors.
The pseudoinverse is , replacing positive singular values by their reciprocals and zero ones by zero. It is a two-sided inverse only for a square nonsingular matrix. Small positive singular values amplify perturbations in the corresponding directions.
See Nocedal & Wright (2006), Appendix A, p.604, Eq.(A.15), for SVD. Activity: compute the SVD of and , reconstruct each matrix, and use the singular values to discuss its rank.
# Matrix Al
print("Matrix = \n", Al, "\n")
U, s, Vh = linalg.svd(Al)
print("U = \n", U, "\n")
print("S = \n", s, "\n")
print("Vh = \n", Vh, "\n")
## Rank (from inspecting singular values)?Matrix =
[[4 3]
[6 3]]
U =
[[-0.59581566 -0.80312122]
[-0.80312122 0.59581566]]
S =
[8.33557912 0.71980602]
Vh =
[[-0.86400595 -0.50348159]
[ 0.50348159 -0.86400595]]
Vector and Matrix Norms¶
A vector norm measures the length of a vector. It maps into the nonnegative reals and satisfies three properties:
The last one is the triangle inequality. The -norm is for , and three cases matter here:
--- the 1-norm
--- the Euclidean norm
--- the max norm
Also for with (using ); the case is the Cauchy-Schwarz inequality.
Induced matrix norms are defined by vector norms:
The rules for the three induced norms are
--- largest column sum
--- largest row sum
the largest singular value of
Finally, the induced norms and the Frobenius norm all satisfy
for any two compatible matrices and . This is submultiplicativity, and it is what the condition number in the next section rests on.
Definition 2.7, pp. 21-22 in Biegler (2010).

The induced 2-norm is because for ; equality holds along a leading right singular vector. Submultiplicativity follows by applying the defining inequality twice: , then taking the maximum over unit vectors.
For continuous finite-dimensional vector- or matrix-valued on with ,
Apply the triangle inequality and homogeneity to each Riemann sum, then pass to the limit. Continuity ensures the sums and their norms converge. Combined with submultiplicativity, this bounds expressions such as by .
x = np.random.rand(3, 1)
print("x = \n", x)
y = np.random.rand(3, 1)
print("y = \n", y)x =
[[0.3595079 ]
[0.43703195]
[0.6976312 ]]
y =
[[0.06022547]
[0.66676672]
[0.67063787]]
Activity
Verify the vector norm properties hold for the p-2 norm using and . Hint: linalg.norm()
Calculate , and using both the rules given above and SciPy
Condition number and residual bounds¶
For nonsingular square and an induced norm,
In the 2-norm, . For real symmetric nonsingular matrices this is ; eigenvalue magnitudes matter. Eigenvalues give . For rectangular full-column-rank matrices the singular-value ratio measures sensitivity of the map on its domain, but there is no square two-sided inverse.

Perturb only the right-hand side¶
Let and . Subtract to get . Also . Therefore
The condition number is a worst-case amplification, not a prediction for every perturbation. With and , a perturbation gives and attains for . Perturbing along the first coordinate is amplified only by one.
For an approximate solution , define the exact residual of the stored data . Then and the same argument gives
A relative residual 10-12 and condition number give the bound 450. This bound does not certify an accurate solution; it also does not establish that the actual forward error is large. Computing the residual in floating point introduces another error. The Numerical Errors experiment evaluates the residual and reference solution in high precision and separates rounding of the input data from error in the solve.
Backward error and stability¶
In the 2-norm, allowing perturbations only in gives
for nonzero . The lower bound follows from ; equality is attained by . If , the finite perturbation bound is
Indeed, ; rearrange. The simpler expression is a first-order rule when that product is small. If perturbations are allowed in both and , the normwise backward error instead has denominator . State which model is used. See Higham (2002), Theorems 7.1–7.2, pp.120–121.
Conditioning describes sensitivity of a problem. Backward stability describes an algorithm’s error over its intended class of inputs. A small measured backward error on one solve is evidence about that solve; it does not prove that the algorithm is backward stable for all inputs.
Activity. Estimate from its SVD and compare with np.linalg.cond. Choose the perturbation direction that produces the greatest change in the answer and explain why.
Four factorizations, four numerical diagnostics¶
These examples isolate element growth in LU, loss of positive definiteness near the precision limit, loss of orthogonality in QR, and the difficulty of computing inertia near zero eigenvalues. Each table is computed by its cell. Exact digits and the first reported failure can depend on NumPy, SciPy, and the BLAS/LAPACK build. Read the actual output of your run.
The Cholesky cell catches an intended LinAlgError if the factorization breaks down. The inertia cell compares floating-point reports with an exact-rational calculation for the stored entries. A returned factorization still needs its defining properties checked: a small reconstruction residual alone does not certify orthogonality, definiteness, or correct inertia.
D1. LU: element growth on a moderately conditioned problem¶
Wilkinson’s matrix has 1 on the diagonal, -1 below it, and a final column of ones. With the pivot tie choices used here, partial pivoting makes no interchanges, and entries grow as during elimination. The table checks the permutation and growth against this exact-arithmetic pattern. Its reported 2-norm condition numbers stay moderate, while the error can become substantial.
The reference vector x_true is chosen first, and b = A @ x_true is rounded. Consequently the reported error includes that assembly error as well as error in the solve. The residual column is normalized by the Frobenius norm of the matrix and the 2-norm of the computed solution, as shown in the code. It is not .
# D1: LU with partial pivoting on Wilkinson's matrix.
import numpy as np
from scipy import linalg
rng = np.random.default_rng(0)
def wilkinson(n):
"""1 on the diagonal, -1 below it, last column all ones."""
A = np.eye(n) - np.tril(np.ones((n, n)), -1)
A[:, -1] = 1.0
return A
print("A for n = 5:")
print(wilkinson(5).astype(int), "\n")
header = "{:>4} {:>8} {:>14} {:>14} {:>14} {:>9} {:>9}"
row = "{:>4d} {:>8.1f} {:>14.1e} {:>14.1e} {:>14.1e} {:>9.1e} {:>9}"
print(header.format("n", "K(A)", "growth", "2^(n-1)", "rel. resid",
"rel. err", "P == I ?"))
for n in [20, 30, 40, 50, 60, 70]:
A = wilkinson(n)
x_true = rng.standard_normal(n)
b = A @ x_true
P, L, U = linalg.lu(A)
growth = np.abs(U).max() / np.abs(A).max()
x_hat = linalg.solve(A, b)
rel_resid = np.linalg.norm(A @ x_hat - b) / (np.linalg.norm(A)
* np.linalg.norm(x_hat))
rel_err = np.linalg.norm(x_hat - x_true) / np.linalg.norm(x_true)
print(row.format(n, np.linalg.cond(A), growth, 2.0 ** (n - 1),
rel_resid, rel_err,
str(np.array_equal(P, np.eye(n)))))
A for n = 5:
[[ 1 0 0 0 1]
[-1 1 0 0 1]
[-1 -1 1 0 1]
[-1 -1 -1 1 1]
[-1 -1 -1 -1 1]]
n K(A) growth 2^(n-1) rel. resid rel. err P == I ?
20 8.8 5.2e+05 5.2e+05 5.6e-13 3.5e-12 True
30 13.3 5.4e+08 5.4e+08 5.1e-10 5.4e-09 True
40 17.8 5.5e+11 5.5e+11 2.6e-07 5.1e-06 True
50 22.3 5.6e+14 5.6e+14 8.2e-05 1.8e-03 True
60 26.8 5.8e+17 5.8e+17 1.2e-02 3.1e-01 True
70 31.3 5.9e+20 5.9e+20 1.5e-02 3.8e-01 True
For this run, inspect P == I ? and compare measured growth with . Ties are relevant: being a largest-magnitude candidate does not force every pivoting implementation to make the same choice. Large element growth weakens the usual backward-error guarantee for elimination; see Higham (2002), §9.4, pp.166–167.
Activity. Compare the error, growth, and condition number columns. Which column identifies a sensitive problem, and which identifies the risk of rounding amplification during elimination? Change the random seed and identify which quantities change.
To use a forward-error bound with this table, first match both the reference problem and the normalization. Its printed residual measure converts to the matrix-only 2-norm backward error by , ignoring error in evaluating the residual. The finite bound requires and compares with the exact solution of the stored system, not automatically with x_true. The high-precision experiment on Numerical Errors makes those distinctions explicitly.
D2. Cholesky near the precision limit¶
The exact Hilbert matrix is symmetric positive definite for every . For any nonzero ,
because a nonzero polynomial cannot vanish throughout the interval. A matrix stored in binary floating point has rounded entries and need not retain this property when the smallest eigenvalues are comparable to roundoff. Computing eigenvalues or a Cholesky factor introduces further rounding. The cell catches breakdown and prints it as a result.
# D2: Cholesky on the Hilbert matrix, which is SPD for every n in exact arithmetic.
import numpy as np
from scipy import linalg
print("{:>4} {:>14} {:>16} {:>24}".format(
"n", "reported K(A)", "smallest comp. e.v.", "scipy.linalg.cholesky"))
for n in [12, 13, 14, 15]:
A = linalg.hilbert(n)
smallest = np.linalg.eigvalsh(A).min()
try:
linalg.cholesky(A)
status = "succeeds"
except np.linalg.LinAlgError:
status = "LinAlgError (expected)"
except linalg.LinAlgError:
status = "LinAlgError (expected)"
print("{:>4d} {:>14.1e} {:>16.1e} {:>24}".format(
n, np.linalg.cond(A), smallest, status))
n reported K(A) smallest comp. e.v. scipy.linalg.cholesky
12 1.6e+16 1.0e-16 succeeds
13 5.1e+17 8.6e-20 succeeds
14 3.0e+17 -5.4e-18 succeeds
15 2.6e+17 -5.5e-18 LinAlgError (expected)
Read the smallest computed eigenvalue and Cholesky status on each row. A negative computed eigenvalue or a Cholesky breakdown does not refute the proof for the exact rational Hilbert matrix. It concerns the stored matrix and floating-point calculation. Conversely, a successful factorization is not a high-accuracy certificate for the exact mathematical data. The numerical condition-number estimate can itself be unreliable near the precision limit.
Activity. A solver uses Cholesky breakdown to trigger a Hessian correction. State what this test reports in finite precision and what extra claim would require error analysis. Extend the loop and compare the first failure on your machine with the displayed output.
D3. QR: reconstruction and orthogonality are separate checks¶
The three columns below differ only in entries of size 10-8. Classical Gram–Schmidt, modified Gram–Schmidt, and Householder QR can all give small reconstruction errors . A QR factorization also requires . The table measures both properties.
# D3: three ways to compute QR, and how orthogonal Q really is.
import numpy as np
from scipy import linalg
def classical_gram_schmidt(A):
m, n = A.shape
Q = np.zeros((m, n))
R = np.zeros((n, n))
for j in range(n):
v = A[:, j].copy()
for i in range(j):
# Classical: every projection is taken against the ORIGINAL column.
R[i, j] = Q[:, i] @ A[:, j]
v = v - R[i, j] * Q[:, i]
R[j, j] = np.linalg.norm(v)
Q[:, j] = v / R[j, j]
return Q, R
def modified_gram_schmidt(A):
m, n = A.shape
Q = np.zeros((m, n))
R = np.zeros((n, n))
V = A.copy()
for j in range(n):
R[j, j] = np.linalg.norm(V[:, j])
Q[:, j] = V[:, j] / R[j, j]
for i in range(j + 1, n):
# Modified: project against the RUNNING remainder, not the original.
R[j, i] = Q[:, j] @ V[:, i]
V[:, i] = V[:, i] - R[j, i] * Q[:, j]
return Q, R
eps = 1e-8
A = np.array([[1.0, 1.0, 1.0],
[eps, 0.0, 0.0],
[0.0, eps, 0.0],
[0.0, 0.0, eps]])
print("A =\n", A)
print("\nK_2(A) = {:.1e}\n".format(np.linalg.cond(A)))
methods = [
("classical Gram-Schmidt", classical_gram_schmidt),
("modified Gram-Schmidt", modified_gram_schmidt),
("Householder (scipy.linalg.qr)", lambda M: linalg.qr(M, mode="economic")),
]
print("{:<32} {:>14} {:>14}".format("method", "||Q'Q - I||_2", "||QR - A||_2"))
for name, factorize in methods:
Q, R = factorize(A)
orth = np.linalg.norm(Q.T @ Q - np.eye(A.shape[1]), 2)
recon = np.linalg.norm(Q @ R - A, 2)
print("{:<32} {:>14.1e} {:>14.1e}".format(name, orth, recon))
A =
[[1.e+00 1.e+00 1.e+00]
[1.e-08 0.e+00 0.e+00]
[0.e+00 1.e-08 0.e+00]
[0.e+00 0.e+00 1.e-08]]
K_2(A) = 1.7e+08
method ||Q'Q - I||_2 ||QR - A||_2
classical Gram-Schmidt 5.0e-01 1.4e-25
modified Gram-Schmidt 8.2e-09 2.5e-25
Householder (scipy.linalg.qr) 2.5e-16 5.0e-24
A reconstruction close to does not make a nonorthogonal a valid orthogonal factor. Compare the actual orthogonality errors with the reconstruction errors. Householder QR is the library computation; the two Gram–Schmidt routines are printed in full so you can inspect their projection order.
Activity. Choose which of the two reported errors is needed before using in a derivation, and explain your reasoning. Then sweep eps from 10-1 to 10-10 and plot both errors for each method. If a null-space method assumes an orthonormal basis, identify the equation that becomes inaccurate when orthogonality is lost.
D4. Symmetric indefinite: the inertia comes back wrong¶
A KKT matrix, with a positive definite Hessian block and two constraint rows that nearly duplicate each other:
is nonsingular for every , so the inertia of - the counts of positive, negative and zero eigenvalues - is exactly, on every row below. The exact column is not an assertion typed in by hand: it is recomputed symbolically with sympy, over the rationals, from the actual double precision entries of .
The wrong answers printed below are the demonstration. They are what this cell exists to show.
# D4: inertia of a nearly degenerate KKT matrix, three ways.
import numpy as np
import sympy
from scipy import linalg
def inertia_exact(K):
"""Exact inertia of the double precision entries, over the rationals."""
M = sympy.Matrix(K.shape[0], K.shape[1],
lambda i, j: sympy.Rational(K[i, j]))
pos = neg = zero = 0
for value, multiplicity in M.eigenvals().items():
v = sympy.N(value, 50)
if v > 0:
pos += multiplicity
elif v < 0:
neg += multiplicity
else:
zero += multiplicity
return (pos, neg, zero)
def inertia_from_block_diagonal(D):
"""Inertia read off the block diagonal D returned by scipy.linalg.ldl."""
pos = neg = zero = 0
i, N = 0, D.shape[0]
while i < N:
if i + 1 < N and D[i, i + 1] != 0.0:
ev = np.linalg.eigvalsh(D[i:i + 2, i:i + 2])
i += 2
else:
ev = np.array([D[i, i]])
i += 1
pos += int((ev > 0).sum())
neg += int((ev < 0).sum())
zero += int((ev == 0).sum())
return (pos, neg, zero)
print("{:>10} {:>14} {:>12} {:>12} {:>12}".format(
"epsilon", "reported K(K)", "exact", "ldl", "eigvalsh"))
for eps in [1e-6, 1e-7, 1e-8, 1e-9, 1e-10]:
H = 2.0 * np.eye(2)
A = np.array([[1.0, 1.0], [1.0, 1.0 + eps]])
K = np.block([[H, A.T], [A, np.zeros((2, 2))]])
_, D, _ = linalg.ldl(K)
ev = np.linalg.eigvalsh(K)
from_eig = (int((ev > 0).sum()), int((ev < 0).sum()), int((ev == 0).sum()))
print("{:>10.0e} {:>14.1e} {:>12} {:>12} {:>12}".format(
eps, np.linalg.cond(K), str(inertia_exact(K)),
str(inertia_from_block_diagonal(D)), str(from_eig)))
epsilon reported K(K) exact ldl eigvalsh
1e-06 2.6e+13 (2, 2, 0) (2, 2, 0) (2, 2, 0)
1e-07 2.6e+15 (2, 2, 0) (2, 2, 0) (2, 2, 0)
1e-08 1.1e+18 (2, 2, 0) (3, 1, 0) (3, 1, 0)
1e-09 5.8e+16 (2, 2, 0) (3, 1, 0) (2, 2, 0)
1e-10 3.2e+17 (2, 2, 0) (3, 1, 0) (3, 1, 0)
No solver, no barrier parameter, no iteration - just a matrix, a factorization, and a wrong answer about it.
Find the row where the two computed columns disagree with each other, so at least one of them is wrong, and use the exact column to see which. On the machine the handout was written on that is , where ldl is wrong and eigvalsh is right; on yours it may be a different row, or there may be more than one. That variability is itself the lesson: two routines in the same library, asked the same question about the same matrix, return different answers, and which one is wrong where depends on the LAPACK build underneath.
Activity
An interior point solver reads the inertia of the KKT matrix at every iteration to decide whether to correct the Hessian. What does this table do to that sentence?
A solver that reads here concludes the reduced Hessian has the wrong curvature and adds a correction it does not need. Describe the mirror case, where the measurement error points the other way. Which is worse?
Where did the ill conditioning come from? Note that it is not a scaling accident: it is two constraints that nearly say the same thing, which approaches a constraint-qualification failure as the rows become identical. Each represented matrix in this table still has independent rows: near dependence is not an exact LICQ failure.
Where each of these comes back. None of the four is a curiosity; each is planted here and paid off later, inside an algorithm that has to cope with it.
| Failure | Comes back as |
|---|---|
| D1, growth | why partial pivoting has a growth-dependent error bound |
| D2, Cholesky | finite-precision checks of positive definiteness; BFGS preserves it under its curvature hypothesis |
| D3, | the cost of forming a null space basis for reduced space methods |
| D4, inertia | what inertia correction in an interior point method is correcting |
| , scaling | degeneracy, rank deficiency and NLP diagnostics |
The algorithmic halves already exist as runnable code: Quasi-Newton Methods for Unconstrained Optimization implements the SR1 update and shows a Cholesky factorization in SciPy, leaving the BFGS update built on it as homework, Inertia-Corrected Newton Method for Equality Constrained NLPs runs inertia correction inside the iteration, and NLP Diagnostics with Degeneracy Hunter is the diagnostic tooling for the last row.
Worked LU, QR, and Cholesky factorizations¶
LU: two triangular solves¶
For and , SciPy’s factors are
First solve . Forward substitution gives and , hence . Back substitution in gives , , hence .
QR: projections and a first-row check¶
For , , with independent columns, the thin QR factorization is with , , and nonsingular upper-triangular . For square , solve . For a tall matrix, the same equation solves the full-column-rank least-squares problem, since the residual decomposes into components in the range of and its orthogonal complement.

Take and .
Normalize the first column: , .
Project the second column: . Subtract this component: .
Normalize the orthogonal remainder: , . Orthogonality follows from .
Assemble and . The dot product of the two columns of is zero.
Check the first row of : , the first row of .
Compute . Back substitution gives , .
The decomposition is the geometric mechanism: a projection along the first unit direction plus a perpendicular remainder. This two-column Gram–Schmidt calculation explains the factors. Library QR uses Householder transformations; sign choices can change columns of and matching rows of while leaving the product unchanged. See Nocedal & Wright (2006), Appendix A, pp.608–609, and Higham (2002), Chapter 19 for numerical analysis.
Cholesky: matching entries of a triangular product¶
A real symmetric positive definite has a unique factorization with lower triangular and . The leading cost is operations. Matching column gives
The radicands are positive under the positive-definite hypothesis in exact arithmetic. Nonpositive pivots can instead occur for singular semidefinite or indefinite matrices; rounding can also cause breakdown near the precision limit. A negative square root is one possible failure, not the only one.

For ,
Thus , , and . With , solve to get ; then solve to get .
For the indefinite , the same recurrence reaches . Its eigenvalues are 3 and -1. For the positive semidefinite matrix of all ones, it instead reaches a zero second diagonal entry, so the strictly positive-diagonal nonsingular factorization is unavailable.
Positive definiteness also follows directly from a nonsingular triangular factor: for nonzero . See Higham (2002), §10.1, pp.196–197.
# Verify the supplied worked examples without imposing a QR sign convention.
from scipy import linalg
Al_worked = np.array([[4., 3.], [6., 3.]])
bl_worked = np.array([1., 0.])
P_worked, L_worked, U_worked = linalg.lu(Al_worked)
y_worked = linalg.solve_triangular(L_worked, P_worked.T @ bl_worked, lower=True)
x_worked = linalg.solve_triangular(U_worked, y_worked)
assert np.allclose(x_worked, [-.5, 1.])
print('LU solution:', x_worked)
Aqr_worked = np.array([[3., 1.], [4., 2.]])
Q_worked = np.array([[3/5, -4/5], [4/5, 3/5]])
R_worked = np.array([[5., 11/5], [0., 2/5]])
assert np.allclose(Q_worked.T @ Q_worked, np.eye(2))
assert np.allclose(Q_worked @ R_worked, Aqr_worked)
print('QR first row:', (Q_worked @ R_worked)[0])
print('QR transformed RHS:', Q_worked.T @ np.array([4., 6.]))
print('QR solution:', linalg.solve_triangular(R_worked, Q_worked.T @ [4., 6.]))
B_worked = np.array([[4., 2.], [2., 5.]])
C_worked = linalg.cholesky(B_worked, lower=True)
assert np.allclose(C_worked, [[2., 0.], [1., 2.]])
print('Cholesky factor:\n', C_worked)
print('Cholesky solution:', linalg.cho_solve((C_worked, True), [6., 7.]))
print('Indefinite contrast eigenvalues:', linalg.eigvalsh([[1., 2.], [2., 1.]]))LU solution: [-0.5 1. ]
QR first row: [3. 1.]
QR transformed RHS: [7.2 0.4]
QR solution: [1. 1.]
Cholesky factor:
[[2. 0.]
[1. 2.]]
Cholesky solution: [1. 1.]
Indefinite contrast eigenvalues: [-1. 3.]
Row and column scaling: recover the original variables¶
This example is used in the closing Optimization for Decision Support lecture. It changes the numerical representation of a linear equality system, while preserving its original solution. Use the induced 2-norm condition number; it is a worst-case perturbation measure, not a prediction of the error in every solve.
For , multiplying equality row by a nonzero constant must multiply as well. With positive diagonal row matrix , the equivalent system is . For inequalities, a negative row multiplier reverses the inequality, so positive residual scales are the usual convention.
When dividing column by , define . Then and . Bounds and any other occurrences of that variable must transform too. Combined row/column scaling gives .
The example has
The following calculation compares no scaling, row division by the largest absolute row entry, column division by the largest absolute column entry, and row-then-column scaling. Each solution is mapped back and checked in the original equations. The unequal improvement is a result for this example; row scaling can help other matrices, and neither scheme guarantees a small condition number for every problem.
import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
if "google.colab" in sys.modules:
import urllib.request
urllib.request.urlretrieve("https://raw.githubusercontent.com/ndcbe/optimization/main/notebooks/helper.py", "helper.py")
else:
sys.path.insert(0, "../")
import helper
helper.set_plotting_style()
scale_A = np.array([[1e-5, 500., .2], [3e-6, 1000., .4], [1e-6, 200., 1.]])
scale_b = np.array([1.7, 1.7, 1.3])
row_div = np.max(np.abs(scale_A), axis=1)
row_A, row_b = scale_A/row_div[:, None], scale_b/row_div
col_div = np.max(np.abs(scale_A), axis=0)
both_div = np.max(np.abs(row_A), axis=0)
scale_cases = [
("original", scale_A, scale_b, np.ones(3)),
("rows", row_A, row_b, np.ones(3)),
("columns", scale_A/col_div, scale_b, col_div),
("rows then columns", row_A/both_div, row_b, both_div),
]
scale_rows = []
for name, matrix, rhs, divisors in scale_cases:
z = np.linalg.solve(matrix, rhs)
recovered = z/divisors
residual = float(np.linalg.norm(scale_A@recovered-scale_b, np.inf))
assert residual < 1e-13
scale_rows.append({"case":name, "condition_2":float(np.linalg.cond(matrix,2)),
"scaled_solution":z.tolist(), "recovered_solution":recovered.tolist(),
"original_residual_inf":residual})
display(pd.DataFrame(scale_rows).set_index("case"))results = {"A":scale_A.tolist(), "b":scale_b.tolist(), "cases":scale_rows,
"row_divisors":row_div.tolist(), "column_divisors":col_div.tolist(),
"both_column_divisors":both_div.tolist()}
helper.save_results("ctx-scaling-actual", results,
notebook="notebooks/6-dev/Math-Primer.ipynb",
source_cells=[("notebooks/6-dev/Math-Primer.ipynb", "results:ctx-scaling-model"),
("notebooks/6-dev/Math-Primer.ipynb", "results:ctx-scaling-archive")],
description="Approved L27 row/column scaling example, with solutions recovered in original units",
solver="NumPy linear solve and singular values", quiet=True)fig, ax = plt.subplots(figsize=(7.6,2.8))
rows = results["cases"]
for i,row in enumerate(rows):
ax.plot([i,i],[1,row["condition_2"]],color="black",linestyle="-",linewidth=2)
ax.plot(i,row["condition_2"],marker="s" if i<2 else "o",color="#0072B2",linestyle="none",ms=7)
ax.annotate(f'{row["condition_2"]:.2g}', (i,row["condition_2"]),
xytext=(0,8),textcoords="offset points",ha="center",fontsize=11)
ax.set_yscale("log")
ax.set(xticks=range(4),xticklabels=["original","rows","columns","rows then\ncolumns"],
ylabel="2-norm condition number",ylim=(1,2e9),xlim=(-.4,3.4))
ax.tick_params(axis="x",labelsize=12)
ax.grid(axis="y",which="major",alpha=.25)
fig.tight_layout()
helper.save_figure(fig,"ctx-scaling-actual",quiet=True)
plt.show()
The original and recovered solution is . Column scaling gives coordinates ; row-then-column scaling gives approximately . The much smaller coordinates in the combined case are not different physical decisions. The 2-norm condition numbers are approximately , , 2.79, and 4.50. They compare conditioning of four represented systems; check the original residual after reversing the transformation. Choosing scales needs characteristic units and model knowledge, not only a blind largest-entry rule. Near dependent equations can remain ill conditioned after unit scaling.