Skip to content
The Lumenary

Math laboratory

Conditioning, pivoting and least squares in floating point

Solves Hilbert systems and compares the error with kappa(H) times the unit roundoff; shows elimination without pivoting failing on [[1e-20, 1], [1, 1]] while partial pivoting succeeds; measures the growth factor 2^(n-1) on Wilkinson's matrix; and shows the normal equations breaking down on Lauchli's least-squares problem while QR works. Floating-point evidence.

Back to the labNumerical approximationCounterexample
Conditioning, pivoting and least squares in floating pointPythonNumerical approximationCounterexampleComputationally tested
"""Conditioning, pivoting and least squares in floating point.

Four numerical experiments in IEEE double precision (unit roundoff
u = 2^-53, about 1.1e-16). (1) Hilbert matrices H_ij = 1/(i + j - 1) are
notoriously ill conditioned: solving H x = b with a backward-stable solver
gives a relative error of roughly kappa(H) u, as the perturbation bound
predicts. (2) Gaussian elimination without pivoting on [[1e-20, 1], [1, 1]]
returns a wrong answer although the matrix is well conditioned; with
partial pivoting the answer is right. (3) Wilkinson's matrix makes partial
pivoting's growth factor equal 2^(n-1), the worst case. (4) For Lauchli's
least-squares problem the normal equations A^T A x = A^T b are exactly
singular in floating point, while a QR factorisation solves it accurately.

Evidence: numerical (floating point), compared with exact solutions.
"""

import numpy as np

UNIT_ROUNDOFF = 2.0 ** -53

def hilbert(n):
    """The n x n Hilbert matrix."""
    i = np.arange(1, n + 1)
    return 1.0 / (i[:, None] + i[None, :] - 1)

def lu_solve(a, b, pivot=True):
    """Solve a x = b by Gaussian elimination (optionally partial pivoting).

    Returns (x, growth) where growth = max |U_ij| / max |A_ij|.
    """
    a = np.array(a, dtype=float)
    b = np.array(b, dtype=float)
    n = len(b)
    scale = np.max(np.abs(a))
    for k in range(n - 1):
        if pivot:
            p = k + int(np.argmax(np.abs(a[k:, k])))
            a[[k, p]] = a[[p, k]]
            b[[k, p]] = b[[p, k]]
        for i in range(k + 1, n):
            m = a[i, k] / a[k, k]
            a[i, k:] -= m * a[k, k:]
            b[i] -= m * b[k]
    x = np.zeros(n)
    for i in range(n - 1, -1, -1):
        x[i] = (b[i] - a[i, i + 1:] @ x[i + 1:]) / a[i, i]
    growth = np.max(np.abs(np.triu(a))) / scale
    return x, growth

def wilkinson(n):
    """1 on the diagonal and last column, -1 below the diagonal."""
    a = np.eye(n) - np.tril(np.ones((n, n)), -1)
    a[:, -1] = 1.0
    return a

def lauchli(eps):
    """Lauchli's 3 x 2 matrix [[1, 1], [eps, 0], [0, eps]]."""
    return np.array([[1.0, 1.0], [eps, 0.0], [0.0, eps]])

def rel_err(x, exact):
    """Relative error in the max norm."""
    return np.max(np.abs(x - exact)) / np.max(np.abs(exact))

def main():
    print("1. Hilbert systems H x = b with exact x = (1, ..., 1)")
    print("    n     kappa(H)   rel. error   kappa * u")
    for n in [4, 6, 8, 10, 12]:
        h = hilbert(n)
        x = np.linalg.solve(h, h @ np.ones(n))
        k = np.linalg.cond(h)
        print(f"   {n:2d}   {k:10.2e}   {rel_err(x, np.ones(n)):10.2e}"
              f"   {k * UNIT_ROUNDOFF:9.2e}")
    print()
    a, b = [[1e-20, 1.0], [1.0, 1.0]], [1.0, 2.0]
    x0, _ = lu_solve(a, b, pivot=False)
    x1, _ = lu_solve(a, b, pivot=True)
    print("2. [[1e-20, 1], [1, 1]] x = (1, 2); exact x = (1, 1) to 20 digits")
    print(f"   no pivoting:      x = ({x0[0]:.6f}, {x0[1]:.6f})")
    print(f"   partial pivoting: x = ({x1[0]:.6f}, {x1[1]:.6f})")
    print()
    print("3. Growth factor of partial pivoting on Wilkinson's matrix")
    for n in [10, 30, 55]:
        w = wilkinson(n)
        xs = np.linspace(1.0, 2.0, n)
        x, g = lu_solve(w, w @ xs)
        print(f"   n = {n:2d}: growth {g:9.3e} = 2^{np.log2(g):.0f},"
              f" rel. error {rel_err(x, xs):.1e}")
    print()
    eps = 1e-8
    m = lauchli(eps)
    rhs = m @ np.array([1.0, 1.0])
    gram = m.T @ m
    q, r = np.linalg.qr(m)
    xq = np.linalg.solve(r, q.T @ rhs)
    print("4. Lauchli least squares, eps = 1e-8, exact x = (1, 1)")
    print(f"   computed A^T A = {gram.tolist()}: det = {np.linalg.det(gram):.1e}")
    print(f"   QR solution: ({xq[0]:.8f}, {xq[1]:.8f})")
    print()
    print("Epistemic status: numerical experiments in IEEE double precision.")
    print("They illustrate the perturbation bound, the need for pivoting, the")
    print("worst-case growth factor and the squaring of the condition number")
    print("by the normal equations; the theorems themselves are in the lesson.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in