Sixteen digits are not enough — or are they?
On 25 February 1991 a Patriot missile battery at Dhahran failed to intercept an incoming missile, and 28 soldiers died. The system counted time in tenths of a second and converted to seconds by multiplying by , stored in a 24-bit register. But has no finite binary expansion, and the stored value was too small by about . After 100 hours of continuous operation the clock had drifted by seconds, during which the target travelled more than half a kilometre (US General Accounting Office report, 1992).
Rounding errors are usually harmless. Every weather forecast, every bridge designed by finite elements and every GPS fix solves enormous systems of equations in floating point, and the answers are excellent. Numerical analysis is the mathematics that explains when and why, and it rests on three questions.
- Is the problem sensitive? Some problems amplify any small change in their data. That is a property of the problem — its condition number — and no algorithm can repair it.
- Is the algorithm stable? A good algorithm adds no more error than the inevitable rounding of the data. Gaussian elimination with pivoting is good; without pivoting it can fail on an innocent system.
- How fast does the method converge? Discretising a differential equation with step gives an error like , where is the order, provided the method is stable — and for "stiff" problems, stability can matter far more than order.
This lesson develops these ideas for the two workhorses of scientific computing: linear algebra (linear systems, least squares and eigenvalues) and ordinary differential equations.
Problems, algorithms, and who is to blame
Think of a problem as a function from data to answers, and of an algorithm as a computed approximation . The forward error is what we care about but usually cannot measure. The backward error asks a cleverer question: for which nearby data is the computed answer exact? If with as small as the rounding of the data, the algorithm is backward stable: it has answered a question we could not distinguish from ours.
Then the error splits into two factors:
The algorithm controls the second factor and the problem controls the first. Geometrically, a matrix maps the unit sphere to an ellipsoid. If the ellipsoid is very thin, two very different inputs have nearly the same image, and inverting magnifies errors by the ratio of its longest to its shortest axis. That ratio is the condition number.
For differential equations the analogue of backward stability is stability of the scheme: errors made in early steps must not be amplified by later ones. A method that is consistent (each step is accurate to some order) and stable converges. Stiff problems contain a rapidly decaying component that is invisible in the solution, but an explicit method must still take tiny steps to keep that component from exploding.
Definitions
Definition— floating-point model
A floating-point system with base 2 and significant bits has unit roundoff ( in IEEE double precision). Every real in range is rounded to with , and the basic operations satisfy , , for .
Definition— norms and condition number
For a vector norm on , the induced matrix norm is , so that and . The condition number of an invertible matrix is . In the 2-norm, , the ratio of the largest and smallest singular values.
Definition— forward and backward error, backward stability
Let an algorithm compute as an approximation to . The forward error is (relative: divided by ). The backward error is the smallest such that . The algorithm is backward stable if its backward error is for all data.
Definition— LU and QR factorisations
An LU factorisation writes with unit lower triangular and upper triangular. With partial pivoting, for a permutation matrix , where at step the row with the largest , , is swapped into the pivot position. The growth factor is . A QR factorisation of (, full rank) writes with having orthonormal columns and upper triangular with positive diagonal (the reduced form).
Definition— one-step methods, order, stability function
For , a one-step method computes . Its local truncation error is for the exact solution . It has order if the global error is for smooth problems. Applied to the test equation , a Runge–Kutta method gives , and is its stability function. The method is A-stable if whenever .
Examples
Example— A nearly singular system
The system , has solution . Change the right-hand side to , a relative change of about , and the solution becomes . Here , , and . The two equations describe almost parallel lines, and a tiny shift of one moves their intersection a long way.
Example— Condition numbers to know
- Orthogonal matrices: , the best possible. Multiplying by them never amplifies errors, which is why QR is so stable.
- Diagonal matrices: in any -norm.
- Hilbert matrices : grows roughly like , so already — numerically singular in double precision.
Example— Euler's method on y′ = λy
Explicit Euler gives , so , while the exact solution is multiplied by per step. For and , : the numerical solution oscillates with growing amplitude while the true one decays like . Backward Euler, , gives here, decaying as it should.
Conditioning of linear systems
Theorem— Theorem 1 (perturbing the right-hand side)
Let be invertible, and . Then
and for each there are and for which equality holds.
Proof
Subtracting, , so and . Also , so . Multiply the two inequalities. For equality, choose with and with ; both exist because the maximum defining an induced norm is attained.
∎
Theorem— Theorem 2 (perturbing the matrix)
Let be invertible and . If and with , then is invertible and
To first order: relative forward error relative backward error.
Proof
If then , so , forcing : is invertible. Expanding and using , we get , so
Move the last term to the left: . Divide by , and use and .
∎
Since rounding the data alone causes relative perturbations of size , no algorithm can promise relative accuracy better than about . The rule of thumb: with , expect to lose about of the significant decimal digits.
Gaussian elimination as LU factorisation
Gaussian elimination subtracts multiples of the pivot row from the rows below it. Each step is multiplication by a unit lower triangular matrix , and the product of their inverses is the unit lower triangular matrix of multipliers. So elimination computes . Solving then takes a forward substitution and a back substitution .
Theorem— Theorem 3 (existence and uniqueness of LU)
An invertible has a factorisation ( unit lower triangular, upper triangular) if and only if all its leading principal submatrices are invertible. The factorisation is then unique.
Proof
Necessity. If , then (the leading blocks multiply, because is lower and upper triangular). Since , every .
Existence, by induction on . Write and, by induction, with invertible. Seek and . Comparing blocks requires , and , which have the unique solutions , and .
Uniqueness. If , then is both unit lower triangular and upper triangular, hence the identity.
∎
The matrix is perfectly well conditioned but has no LU factorisation, since . A nearly zero pivot is just as bad in floating point. For the multiplier is and . With , , and the computed solution of is instead of . Swapping the rows first gives multiplier and an accurate answer. Partial pivoting always selects the largest available pivot. Then every , and every invertible has a factorisation : at each step the pivot column below the diagonal contains a non-zero entry, since otherwise the matrix would be singular.
Theorem— Theorem 4 (Wilkinson's backward error analysis; stated)
Gaussian elimination with partial pivoting, in floating point, computes with , where for a modest constant (a low-degree polynomial in ) and the growth factor . Moreover , with equality for a matrix of Wilkinson's ( on the diagonal and in the last column, below the diagonal).
(Wilkinson, 1961; see Higham, Accuracy and Stability of Numerical Algorithms (2002), chapter 9.) The bound is attained, so partial pivoting is not backward stable in the worst case. In practice growth factors are small — of order on average for random matrices, according to experiments of Trefethen and Schreiber (1990) — and matrices with large growth seem almost never to arise. Why this is so is still not fully understood. It is an empirical fact supported by probabilistic analysis, not a theorem about all matrices. LU costs about flops; each additional right-hand side costs only .
Do not compute to solve . Forming costs about flops, three times as much as LU. The computed is also not backward stable: its residual can be about times larger than the residual of the LU solution, although both forward errors are of size (Higham 2002, §14.1). And for sparse , the inverse is typically dense while the LU factors often stay sparse.
Least squares, QR and eigenvalues
Given with and full column rank, and , the least-squares problem asks for minimising — for example, fitting a line to noisy measurements.
Theorem— Theorem 5 (normal equations and QR)
(a) minimises if and only if , and the minimiser is unique. (b) If (reduced QR factorisation), the minimiser is the solution of the triangular system . (c) .
Proof
(a) For any and , . If , the middle term vanishes and is a minimiser, unique because for (full rank). Conversely, at a minimiser the derivative of the quadratic must vanish.
(b) and , so the normal equations read . Since is invertible, this means .
(c) With the singular value decomposition , has singular values .
∎
So solving the normal equations in floating point loses twice as many digits as the problem deserves. In Läuchli's example with , , and makes it exactly singular. QR computed by Householder reflections works on itself and is backward stable (Higham 2002, chapter 19). Orthogonal transformations do not amplify errors, since .
Eigenvalues by the QR algorithm. Starting from , factor and set . Since , every is orthogonally similar to and has the same eigenvalues. Theorem (stated): if the eigenvalues of have distinct absolute values (plus a mild condition on the eigenvectors), then converges to an upper triangular matrix with the on the diagonal. The subdiagonal entry decays like . The reason is that is the orthonormalisation of — simultaneous power iteration on all columns at once. Practical versions first reduce to Hessenberg form (cost once, then per step) and use shifts , which make convergence typically quadratic, or cubic for symmetric matrices (Trefethen and Bau 1997, lectures 28–29).
Numerical solution of ODEs: order and stability
Consider , on , with continuous and -Lipschitz in : . Take steps , .
Theorem— Theorem 6 (Euler's method converges with order 1)
If the exact solution is with on , the Euler iterates satisfy
Proof
Consistency. By Taylor's theorem, with .
Error recursion. Subtract the Euler step. For , the Lipschitz condition gives
Stability (discrete Gronwall). By induction, . With and (from ), this gives .
∎
The proof has the shape of every convergence proof in numerical analysis: consistency (small local errors) plus stability (errors are not amplified too much) gives convergence. The whole argument is formalised in Lean below.
Runge–Kutta methods evaluate several times per step to cancel more terms of the Taylor expansion. Heun's method , with and , has order 2. The classical fourth-order method of Kutta (1901),
has order 4. Theorem (stated): a Runge–Kutta method whose local truncation error is for smooth (equivalently, whose coefficients satisfy Butcher's order conditions up to order ) has global error , by the same consistency-plus-stability argument as Theorem 6. Checking that the classical method satisfies the eight order conditions for is a finite computation (Hairer, Nørsett and Wanner, Solving Ordinary Differential Equations I, §II.2). The experiment below measures the orders .
Absolute stability. On the test equation with , whose solutions decay, we want the numerical solution to decay too, i.e. . The stability functions are:
| method | stable for real | |
|---|---|---|
| explicit Euler | ||
| classical RK4 | ||
| backward Euler | all | |
| trapezoidal rule | all |
Theorem— Theorem 7 (explicit methods are never A-stable)
(a) Backward Euler and the trapezoidal rule are A-stable. (b) No explicit Runge–Kutta method is A-stable.
Proof
(a) For : , so ; and , because is at least as close to as to . (b) For an explicit method with stages, is a polynomial of degree at most , non-constant since . A non-constant polynomial is unbounded on the half-line .
∎
A problem is stiff when it contains decay rates far faster than the time scale of interest. The equation has the smooth solution after a transient of length , yet explicit Euler needs for ever after. In chemical kinetics or circuit simulation, rates can differ by factors of , and only implicit methods are practical. Germund Dahlquist (1963) proved the second Dahlquist barrier: an A-stable linear multistep method has order at most . Among the A-stable multistep methods of order 2, the trapezoidal rule has the smallest error constant.
Smaller is not always better. Each step makes a rounding error of order , and steps accumulate roughly of it. So the total error behaves like , which is smallest at a moderate and grows as . The same trade-off appears in a single difference quotient: has truncation error and rounding error , optimal near .
Numerical analysis in Lean and Mathlib
Mathlib has operator norms, matrices and determinants, but no model of floating-point arithmetic and no library of numerical algorithms. So what can be formalised is the exact mathematics behind the error analysis: perturbation bounds, factorisations, and the convergence and stability estimates. The rounding-error statements (Theorem 4, backward stability of Householder QR) are not formalised.
The condition number of an invertible bounded linear map between normed spaces, and Theorem 1 in multiplied-out form (which avoids dividing by and ):
/-- The **condition number** `κ(A) = ‖A‖ ‖A⁻¹‖` of an invertible bounded linear map between normed
spaces (for matrices, with the operator norm induced by the chosen vector norm). -/
noncomputable def condNum {E F : Type*} [NormedAddCommGroup E] [NormedSpace ℝ E]
[NormedAddCommGroup F] [NormedSpace ℝ F] (A : E ≃L[ℝ] F) : ℝ :=
‖(A : E →L[ℝ] F)‖ * ‖(A.symm : F →L[ℝ] E)‖/-- The defining property of the operator norm: `‖A x‖ ≤ ‖A‖ ‖x‖`. This applies Mathlib's
`ContinuousLinearMap.le_opNorm`. -/
theorem op_norm_bound {E F : Type*} [NormedAddCommGroup E] [NormedSpace ℝ E]
[NormedAddCommGroup F] [NormedSpace ℝ F] (A : E →L[ℝ] F) (x : E) : ‖A x‖ ≤ ‖A‖ * ‖x‖ :=
A.le_opNorm x/-- **`κ(A) ≥ 1`.** For `x ≠ 0`, `‖x‖ = ‖A⁻¹(A x)‖ ≤ ‖A⁻¹‖ ‖A‖ ‖x‖`; divide by `‖x‖`. A condition
number near `1` (an orthogonal matrix in the 2-norm) is as good as it gets. -/
theorem one_le_condNum {E F : Type*} [NormedAddCommGroup E] [NormedSpace ℝ E]
[NormedAddCommGroup F] [NormedSpace ℝ F] (A : E ≃L[ℝ] F) {x : E} (hx : x ≠ 0) :
1 ≤ condNum A := by
have hxpos : 0 < ‖x‖ := norm_pos_iff.mpr hx
have h1 : ‖x‖ ≤ ‖(A.symm : F →L[ℝ] E)‖ * ‖A x‖ := by
have := (A.symm : F →L[ℝ] E).le_opNorm (A x)
simpa using this
have h2 : ‖A x‖ ≤ ‖(A : E →L[ℝ] F)‖ * ‖x‖ := (A : E →L[ℝ] F).le_opNorm x
have h3 : ‖x‖ ≤ condNum A * ‖x‖ := by
calc ‖x‖ ≤ ‖(A.symm : F →L[ℝ] E)‖ * ‖A x‖ := h1
_ ≤ ‖(A.symm : F →L[ℝ] E)‖ * (‖(A : E →L[ℝ] F)‖ * ‖x‖) :=
mul_le_mul_of_nonneg_left h2 (norm_nonneg _)
_ = condNum A * ‖x‖ := by unfold condNum; ring
nlinarith/-- **The perturbation bound.** If `A x = b` and `A (x + δx) = b + δb`, then
`‖δx‖ · ‖b‖ ≤ κ(A) · ‖δb‖ · ‖x‖`, i.e. `‖δx‖/‖x‖ ≤ κ(A) ‖δb‖/‖b‖`: the relative error in the
solution is at most the condition number times the relative error in the data. Proof: by
linearity `A δx = δb`, so `δx = A⁻¹ δb` and `‖δx‖ ≤ ‖A⁻¹‖ ‖δb‖`; and `‖b‖ = ‖A x‖ ≤ ‖A‖ ‖x‖`.
Multiply the two inequalities. -/
theorem relative_error_bound {E F : Type*} [NormedAddCommGroup E] [NormedSpace ℝ E]
[NormedAddCommGroup F] [NormedSpace ℝ F] (A : E ≃L[ℝ] F) {x δx : E} {b δb : F}
(hb : A x = b) (hpert : A (x + δx) = b + δb) :
‖δx‖ * ‖b‖ ≤ condNum A * ‖δb‖ * ‖x‖ := by
have hAδ : A δx = δb := by
rw [map_add, hb] at hpert
exact add_left_cancel hpert
have h1 : ‖δx‖ ≤ ‖(A.symm : F →L[ℝ] E)‖ * ‖δb‖ := by
have := (A.symm : F →L[ℝ] E).le_opNorm δb
rw [← hAδ] at this ⊢
simpa using this
have h2 : ‖b‖ ≤ ‖(A : E →L[ℝ] F)‖ * ‖x‖ := by
rw [← hb]; exact (A : E →L[ℝ] F).le_opNorm x
calc ‖δx‖ * ‖b‖ ≤ (‖(A.symm : F →L[ℝ] E)‖ * ‖δb‖) * (‖(A : E →L[ℝ] F)‖ * ‖x‖) :=
mul_le_mul h1 h2 (norm_nonneg _) (by positivity)
_ = condNum A * ‖δb‖ * ‖x‖ := by unfold condNum; ringAn LU factorisation produced by elimination, and the determinant as the product of the pivots:
/-- **An LU factorisation**, as produced by Gaussian elimination: subtract `2 ×` row 1 from row 2
and `4 ×` row 1 from row 3, then `3 ×` the new row 2 from row 3. The multipliers `2, 4, 3` fill
the unit lower triangular factor `L`; the result of elimination is the upper triangular `U`.
Checked by exact rational arithmetic. -/
theorem lu_example :
!![(2 : ℚ), 1, 1; 4, 3, 3; 8, 7, 9] = !![(1 : ℚ), 0, 0; 2, 1, 0; 4, 3, 1] * !![(2 : ℚ), 1, 1; 0, 1, 1; 0, 0, 2] := by
ext i j
fin_cases i <;> fin_cases j <;> simp [Matrix.mul_apply, Fin.sum_univ_succ] <;> norm_num/-- `det (L U) = det L · det U`. This applies Mathlib's `Matrix.det_mul`. With `L` unit lower
triangular (`det L = 1`) and `U` upper triangular, `det A` is the product of the pivots — the
cheapest way to compute a determinant (about `2n³/3` operations instead of `n!` terms). -/
theorem det_mul_lu {n : ℕ} (L U : Matrix (Fin n) (Fin n) ℚ) : (L * U).det = L.det * U.det :=
Matrix.det_mul L U/-- The determinant of the example matrix is the product of the pivots `2 · 1 · 2 = 4`, computed
through the factorisation `lu_example` (`det L = 1` and `det U = 4`, each by the formula for a
3 × 3 determinant). -/
theorem det_via_lu : (!![(2 : ℚ), 1, 1; 4, 3, 3; 8, 7, 9]).det = 4 := by
rw [lu_example, Matrix.det_mul, Matrix.det_fin_three, Matrix.det_fin_three]
simp
norm_numThe pivoting example, exactly, with and without the row swap:
/-- **Why pivot: elimination without a row swap.** For `ε ≠ 0`,
`[[ε, 1], [1, 1]] = [[1, 0], [1/ε, 1]] · [[ε, 1], [0, 1 - 1/ε]]`. When `ε` is tiny the multiplier
`1/ε` and the entry `1 - 1/ε` are huge, and in floating point `1 - 1/ε` rounds to `-1/ε`,
destroying the information in the `(2, 2)` entry: the computed solution can be completely wrong
although the matrix is perfectly well conditioned. -/
theorem no_pivot_lu {ε : ℝ} (hε : ε ≠ 0) :
!![ε, 1; 1, 1] = !![1, 0; 1 / ε, 1] * !![ε, 1; 0, 1 - 1 / ε] := by
ext i j
fin_cases i <;> fin_cases j <;> simp [Matrix.mul_apply, Fin.sum_univ_succ]
field_simp/-- **With partial pivoting.** Swapping the rows first (`P A` with `P` the swap) gives
`[[1, 1], [ε, 1]] = [[1, 0], [ε, 1]] · [[1, 1], [0, 1 - ε]]`: the multiplier is `ε`, at most `1`
in absolute value, and no entry grows. Partial pivoting always chooses the largest available
pivot, so every multiplier satisfies `|ℓᵢⱼ| ≤ 1`. -/
theorem pivot_lu (ε : ℝ) :
!![1, 1; ε, 1] = !![1, 0; ε, 1] * !![(1 : ℝ), 1; 0, 1 - ε] := by
ext i j
fin_cases i <;> fin_cases j <;> simp [Matrix.mul_apply, Fin.sum_univ_succ]Theorem 6, Euler's method has order 1, in three steps that follow the human proof — the error recursion, the discrete Gronwall inequality, and the global bound (with from Mathlib):
/-- **One step of the error recursion.** Let `yₙ₊₁ = yₙ + h f(tₙ, yₙ)` (Euler) and let the exact
solution satisfy `y(tₙ₊₁) = y(tₙ) + h f(tₙ, y(tₙ)) + τₙ` (`τₙ` is the local truncation error).
If `f` is `L`-Lipschitz in `y`, the error `eₙ = y(tₙ) - yₙ` satisfies
`|eₙ₊₁| ≤ (1 + h L) |eₙ| + |τₙ|`. -/
theorem euler_error_step {f : ℝ → ℝ → ℝ} {L h t yn Y τ : ℝ} (hh : 0 ≤ h)
(hlip : ∀ s u v, |f s u - f s v| ≤ L * |u - v|) :
|(Y + h * f t Y + τ) - (yn + h * f t yn)| ≤ (1 + h * L) * |Y - yn| + |τ| := by
have hf := hlip t Y yn
calc |(Y + h * f t Y + τ) - (yn + h * f t yn)|
= |(Y - yn) + h * (f t Y - f t yn) + τ| := by ring_nf
_ ≤ |Y - yn| + |h * (f t Y - f t yn)| + |τ| := abs_add_three _ _ _
_ = |Y - yn| + h * |f t Y - f t yn| + |τ| := by rw [abs_mul, abs_of_nonneg hh]
_ ≤ |Y - yn| + h * (L * |Y - yn|) + |τ| := by gcongr
_ = (1 + h * L) * |Y - yn| + |τ| := by ring/-- **Discrete Gronwall inequality.** If `eₙ₊₁ ≤ (1 + h L) eₙ + τ` with `h L ≥ 0` and `τ ≥ 0`,
then `eₙ ≤ (1 + h L)ⁿ e₀ + n τ (1 + h L)ⁿ`. Errors committed at each step are amplified by at most
`(1 + h L)` per subsequent step. Induction on `n`. -/
theorem discrete_gronwall {e : ℕ → ℝ} {h L τ : ℝ} (hhL : 0 ≤ h * L) (hτ : 0 ≤ τ)
(hstep : ∀ n, e (n + 1) ≤ (1 + h * L) * e n + τ) (n : ℕ) :
e n ≤ (1 + h * L) ^ n * e 0 + n * τ * (1 + h * L) ^ n := by
have hq : 1 ≤ 1 + h * L := by linarith
induction n with
| zero => simp
| succ n ih =>
have hpow : 1 ≤ (1 + h * L) ^ (n + 1) := one_le_pow₀ hq
calc e (n + 1) ≤ (1 + h * L) * e n + τ := hstep n
_ ≤ (1 + h * L) * ((1 + h * L) ^ n * e 0 + n * τ * (1 + h * L) ^ n) + τ := by
gcongr
_ = (1 + h * L) ^ (n + 1) * e 0 + n * τ * (1 + h * L) ^ (n + 1) + τ := by ring
_ ≤ (1 + h * L) ^ (n + 1) * e 0 + n * τ * (1 + h * L) ^ (n + 1)
+ τ * (1 + h * L) ^ (n + 1) := by nlinarith
_ = (1 + h * L) ^ (n + 1) * e 0 + ((n + 1 : ℕ) : ℝ) * τ * (1 + h * L) ^ (n + 1) := by
push_cast; ring/-- `1 + x ≤ eˣ` for every real `x`, so `(1 + h L)ⁿ ≤ e^{n h L}`. This applies Mathlib's
`Real.add_one_le_exp`. -/
theorem one_add_le_exp (x : ℝ) : 1 + x ≤ Real.exp x := by
rw [add_comm]; exact Real.add_one_le_exp x/-- **Euler's method has order 1.** Suppose the errors satisfy `e₀ = 0` and
`eₙ₊₁ ≤ (1 + h L) eₙ + C h²` (the recursion of `euler_error_step`, with local truncation error
`|τₙ| ≤ C h²`, e.g. `C = max |y''| / 2`). Then at time `T = n h`,
`eₙ ≤ C h · T · e^{L T}`. For fixed `T`, the error is `O(h)`: halving the step halves the error. -/
theorem euler_global_error {e : ℕ → ℝ} {h L C : ℝ} (hh : 0 ≤ h) (hL : 0 ≤ L) (hC : 0 ≤ C)
(h0 : e 0 = 0) (hstep : ∀ n, e (n + 1) ≤ (1 + h * L) * e n + C * h ^ 2) (n : ℕ) :
e n ≤ C * h * (n * h) * Real.exp (L * (n * h)) := by
have hhL : 0 ≤ h * L := mul_nonneg hh hL
have hg := discrete_gronwall hhL (by positivity) hstep n
rw [h0, mul_zero, zero_add] at hg
have hpow : (1 + h * L) ^ n ≤ Real.exp (L * (n * h)) := by
calc (1 + h * L) ^ n ≤ Real.exp (h * L) ^ n :=
pow_le_pow_left₀ (by linarith) (one_add_le_exp _) n
_ = Real.exp (L * (n * h)) := by rw [← Real.exp_nat_mul]; ring_nf
calc e n ≤ n * (C * h ^ 2) * (1 + h * L) ^ n := hg
_ ≤ n * (C * h ^ 2) * Real.exp (L * (n * h)) := by
apply mul_le_mul_of_nonneg_left hpow; positivity
_ = C * h * (n * h) * Real.exp (L * (n * h)) := by ringStability on the test equation: explicit Euler multiplies by and is stable exactly when , while backward Euler contracts for every step size:
/-- **Explicit Euler on `y' = λ y`** is `yₙ₊₁ = yₙ + h λ yₙ = (1 + hλ) yₙ`, so after `n` steps
`yₙ = (1 + hλ)ⁿ y₀`. The exact solution is `e^{λ n h} y₀`. -/
theorem explicit_euler_iterate (h lam y₀ : ℝ) (n : ℕ) :
(fun y => y + h * (lam * y))^[n] y₀ = (1 + h * lam) ^ n * y₀ := by
induction n with
| zero => simp
| succ n ih => rw [Function.iterate_succ_apply', ih]; ring/-- **Stability of explicit Euler.** For a decaying equation (`λ < 0`) and step `h > 0`, the
numerical solution decays (`|1 + hλ| < 1`) exactly when `h |λ| < 2`. For a stiff equation with
`λ = -1000` this forces `h < 0.002`, whatever accuracy is wanted. -/
theorem explicit_euler_stable_iff {h lam : ℝ} (hh : 0 < h) (hlam : lam < 0) :
|1 + h * lam| < 1 ↔ h * (-lam) < 2 := by
have : 0 < h * (-lam) := mul_pos hh (by linarith)
rw [abs_lt]
constructor
· rintro ⟨h1, -⟩; linarith
· intro h2; constructor <;> nlinarith/-- **Backward Euler is unconditionally stable on decaying modes.** The implicit step
`yₙ₊₁ = yₙ + h λ yₙ₊₁` gives `yₙ₊₁ = yₙ / (1 - hλ)`, and for `λ < 0` and every `h > 0` the factor
`1/(1 - hλ)` lies strictly between `0` and `1`: the numerical solution decays, like the true
one, with no restriction on the step size (A-stability, on the negative real axis). -/
theorem backward_euler_contracts {h lam : ℝ} (hh : 0 < h) (hlam : lam < 0) :
0 < 1 / (1 - h * lam) ∧ 1 / (1 - h * lam) < 1 := by
have hpos : 1 < 1 - h * lam := by nlinarith
constructor
· positivity
· rw [div_lt_one (by linarith)]; exact hposWhat is and is not formalised. Proved here: , Theorem 1, the example factorisations, the Euler error recursion, discrete Gronwall and Theorem 6 (given the one-step recursion), and Theorem 7 on the negative real axis for the two Euler methods. Cited from Mathlib: the operator-norm inequality, and . Not formalised: Theorem 2, Theorems 3–5 in general, the QR algorithm, the order of RK4, and anything about floating-point rounding.
Experiments in double precision
The first program puts the linear-algebra theorems to work in IEEE double precision. Hilbert systems are solved with relative error close to , as Theorems 1–2 predict for a backward-stable solver. (The right-hand side is itself rounded, which already causes errors of this size.) Elimination without pivoting fails on the -matrix. Wilkinson's matrix achieves the worst-case growth and destroys all accuracy at , although its condition number is modest. And the normal equations for Läuchli's problem are exactly singular in floating point, while QR solves it.
"""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()The second program measures the order of three one-step methods by halving — the error ratios approach , and — then shows stiffness: explicit Euler on explodes for , is neutral at , and is accurate for , while backward Euler is accurate for every step. Finally it shows the U-shaped total error of a difference quotient as shrinks past .
"""Order, stiffness and rounding: one-step methods for ODEs.
(1) Observed order: solve y' = y, y(0) = 1 on [0, 1] with Euler, Heun
(a second-order Runge-Kutta method) and classical RK4, halving the step
each time. If the global error is C h^p, halving h divides it by 2^p, so
log2 of successive error ratios estimates the order p: we see 1, 2 and 4.
(2) Stiffness: y' = -50 (y - cos t), y(0) = 0 has a smooth solution, but
explicit Euler is stable only for h * 50 < 2. With h = 0.05 it explodes;
backward Euler is stable for every h. (3) Rounding: the difference
quotient (e^(1+h) - e)/h approximates e with truncation error ~ h e/2 and
rounding error ~ u e/h, so the total error is smallest near h = sqrt(u)
and grows again for smaller h.
Evidence: numerical, compared with exact solutions.
"""
import math
def euler_step(f, t, y, h):
"""Explicit Euler."""
return y + h * f(t, y)
def heun_step(f, t, y, h):
"""Heun's method (explicit trapezoid rule), order 2."""
k1 = f(t, y)
k2 = f(t + h, y + h * k1)
return y + h * (k1 + k2) / 2
def rk4_step(f, t, y, h):
"""Classical fourth-order Runge-Kutta method."""
k1 = f(t, y)
k2 = f(t + h / 2, y + h * k1 / 2)
k3 = f(t + h / 2, y + h * k2 / 2)
k4 = f(t + h, y + h * k3)
return y + h * (k1 + 2 * k2 + 2 * k3 + k4) / 6
def integrate(step, f, y0, t_end, n):
"""Take n equal steps of a one-step method from t = 0."""
h = t_end / n
t, y = 0.0, y0
for _ in range(n):
y = step(f, t, y, h)
t += h
return y
def backward_euler_stiff(lam, y0, t_end, n):
"""Backward Euler for y' = lam (y - cos t): solve the linear step exactly."""
h = t_end / n
t, y = 0.0, y0
for _ in range(n):
t += h
y = (y - h * lam * math.cos(t)) / (1 - h * lam)
return y
def stiff_exact(t):
"""Exact solution of y' = -50 (y - cos t), y(0) = 0."""
a, b = 2500 / 2501, 50 / 2501
return a * math.cos(t) + b * math.sin(t) - a * math.exp(-50 * t)
def main():
f = lambda t, y: y
print("1. y' = y on [0, 1]: error at t = 1 and observed order")
print(" n Euler order Heun order RK4 order")
prev = None
for n in [10, 20, 40, 80]:
errs = [abs(integrate(s, f, 1.0, 1.0, n) - math.e)
for s in (euler_step, heun_step, rk4_step)]
cells = []
for k, e in enumerate(errs):
order = "" if prev is None else f"{math.log2(prev[k] / e):.2f}"
cells.append(f"{e:9.2e} {order:>5}")
print((f" {n:4d} " + " ".join(cells)).rstrip())
prev = errs
print()
g = lambda t, y: -50 * (y - math.cos(t))
print("2. Stiff: y' = -50 (y - cos t), y(0) = 0; error at t = 1")
for n in [20, 25, 40, 100]:
e1 = abs(integrate(euler_step, g, 0.0, 1.0, n) - stiff_exact(1.0))
e2 = abs(backward_euler_stiff(-50, 0.0, 1.0, n) - stiff_exact(1.0))
print(f" h = {1 / n:5.3f} (50h = {50 / n:4.2f}): explicit {e1:9.2e},"
f" backward {e2:8.2e}")
print()
print("3. Forward difference for (e^x)' at x = 1: total error")
for k in [2, 4, 6, 8, 10, 12, 14]:
h = 10.0 ** -k
err = abs((math.exp(1 + h) - math.e) / h - math.e)
print(f" h = 1e-{k:02d}: error {err:.1e}")
print()
print("Epistemic status: numerical experiments. The orders 1, 2, 4, the")
print("stability limit 50h < 2 and the optimal h near sqrt(u) are")
print("theorems or estimates proved in the lesson; these runs illustrate")
print("them on single examples and prove nothing in general.")
if __name__ == "__main__":
main()These are numerical experiments on single examples. They illustrate the theorems, and the agreement with the predicted rates is good evidence that the code is right, but they prove nothing about other matrices or equations.
Explore: thin ellipses, stability regions and stiffness
Ill-conditioning. This matrix has determinant : it squashes the unit square into a sliver, and its condition number is about . Move the entry towards and watch the parallelogram collapse onto a line — the matrix becomes singular, and .
An ill-conditioned matrix
A thin image means a large condition number.
- image of the grid
- image of the unit square
- image of the flag
- before the transformation
- invariant line
The columns of A are the images of e₁ = (1, 0) and e₂ = (0, 1): Ae₁ = (1, 1), Ae₂ = (1, 1.05)
det A = ad − bc = 0.05
The unit square (area 1) becomes a parallelogram of area |det A| = 0.05. Since det A > 0, orientation is preserved.
Invariant lines (real eigenvectors)
- λ₁ = 2.025, v₁ = (0.975, 1)
- λ₂ = 0.025, v₂ = (1, −0.975)
Vectors on the two dashed lines are only stretched, by the factors λ₁ = 2.025 and λ₂ = 0.025; every other direction is turned.
Stability functions on the negative real axis. Each curve is for . A method is stable where its curve lies below the line : explicit Euler on , RK4 on about , and backward Euler everywhere.
|R(hλ)| for three methods
Move the sliders and trace along the curves to read off values.
- explicit Euler
- RK4
- backward Euler
- y = 1
- explicit Euler: y = 1.5
- RK4: y = 1.6484
- backward Euler: y = 2
- y = 1: y = 1
Stiffness in the slope field. Solutions of from any starting value collapse within onto the slow solution . An accurate method only needs to follow that smooth curve, but an explicit method must still respect the fast decay rate .
A stiff equation: y′ = −50(y − cos x)
Each short segment has the slope dy/dx at its point. Click anywhere to draw the solution through that point.
dy/dx = −50·(y − cos(x))
Click the field to draw a solution, or type a start point below.
Solution curves: 3. They are numerical approximations (fourth-order Runge–Kutta), not exact formulas.
Latest curve through (0, 0).
Exercises
1. Numerical analysis
Warm-upHumanWhat is the condition number of ?
2. Numerical analysis
Warm-upHumanA solver returns for , and you find that is the exact solution of with . What can you conclude?
3. Numerical analysis
CoreHumanYou solve in IEEE double precision () with a backward-stable method, and . Roughly how many correct significant decimal digits can you expect in the solution?
4. Numerical analysis
CoreHumanWhich of these invertible matrices has no factorisation (with unit lower triangular and upper triangular) unless rows are swapped?
5. Numerical analysis
CoreHumanWhy should you solve by LU with partial pivoting rather than by computing and multiplying? Choose all correct reasons.
6. Numerical analysis
CoreFormalWhich Lean conclusion correctly expresses Theorem 1, , without dividing by norms that might be zero?
7. Numerical analysis
CoreHumanExplicit Euler is applied to on with equal steps (). What is the smallest for which the numerical solution decays, i.e. ?
8. Numerical analysis
CoreComputationalWrite rk4(f, y0, t_end, n) that applies steps of the classical fourth-order Runge–Kutta method to , , with step , and returns the approximation to .
9. Numerical analysis
StretchHumanLet () have full column rank and 2-norm condition number . Which statements are true? Choose all that apply.
10. Numerical analysis
StretchHumanWhich of these methods are A-stable, i.e. for every complex with ? Choose all that apply.
Common misconceptions
Misconception
"To solve , compute and multiply." It costs about three times as much as LU with partial pivoting, it destroys sparsity, and it is less accurate in the backward sense: the residual of can be times larger. In MATLAB, Julia or NumPy, write A \ b or numpy.linalg.solve(A, b), which use LU. You almost never need itself; if you need , solve .
Misconception
"A smaller step size always reduces the error." Truncation error falls like , but rounding error accumulates over steps and grows like . Below an optimal the total error increases, which is visible in the difference-quotient experiment, where the best is near . For stiff problems the opposite surprise occurs: a larger step with an explicit method does not just lose accuracy, it explodes.
Misconception
"A small residual means an accurate solution." A backward-stable solver produces a residual of order — tiny — even when the forward error is of order and therefore large. The Hilbert matrix of order gives residuals near and errors near . The residual measures the backward error, and accuracy needs the condition number too.
Misconception
"Ill-conditioning is the algorithm's fault." Conditioning belongs to the problem. If , then perturbing the data in the 16th digit changes the answer in the 4th, and no algorithm working with rounded data can do better. The cure is to reformulate the problem (rescale, choose a better basis, add regularisation) or to compute with more digits, not to look for a cleverer solver.
Misconception
"Higher order is always better." Order describes the error as for smooth problems. For stiff problems the explicit RK4 method, whatever its order, is limited by its stability interval of about , while the first-order backward Euler method can take steps a thousand times larger. For non-smooth problems, high-order methods lose their advantage.
Many ways of seeing
Geometry— Distance to singularity, and integrators that respect geometry
The condition number is a distance. Theorem (Eckart–Young, Gastinel–Kahan): , so is the relative distance from to the nearest singular matrix. (Proof: if is singular, Theorem 2's argument shows ; conversely, subtracting from the SVD reaches a singular matrix.) Ill-conditioned problems are those near the "variety of ill-posed problems" — a geometric picture that Demmel (1987) extended to many other problems.
For differential equations, geometric integration asks methods to preserve the structure of the flow. Explicit Euler applied to the harmonic oscillator , multiplies the energy by at every step, so the orbit spirals outwards. The symplectic Euler method, , , preserves area in phase space exactly, and its energy error stays bounded for exponentially long times. That is why symplectic methods are used for planetary orbits and molecular dynamics. See dynamical systems.
Analysis— Consistency + stability = convergence
Theorem 6 is a special case of a general principle. The Lax equivalence theorem (Lax and Richtmyer, 1956) says that for a consistent finite-difference approximation of a well-posed linear initial-value problem, stability is necessary and sufficient for convergence. The explicit heat-equation scheme of partial differential equations is the standard example: consistent for all , stable (hence convergent) exactly for . In the language of functional analysis, stability is a uniform bound on the powers of the discrete evolution operators, and the theorem is an application of the uniform boundedness principle. Backward error analysis has an analytic counterpart for ODEs too: a numerical integrator follows, to high accuracy, the exact flow of a modified differential equation, and studying that equation explains long-time behaviour.
Algebra— Factorisations are group decompositions
The factorisations of numerical linear algebra are classical decompositions of the group . Gram–Schmidt proves the Iwasawa (QR) decomposition: every invertible is uniquely with orthogonal and upper triangular with positive diagonal, so as a product of subgroups. LU with pivoting is the Bruhat decomposition over permutation matrices (with the lower and upper triangular groups); the cell of the identity — matrices with an LU factorisation — is open and dense. The QR algorithm has a hidden integrable structure: its iterates are samples of the Toda flow, a Hamiltonian system on symmetric matrices that preserves eigenvalues (Symes, 1982; Deift, Nanda and Tomei, 1983).
Probability— Random matrices and why pivoting works
Worst-case analysis says partial pivoting can lose everything (); experience says it never does. Probability bridges the gap. For an matrix with independent standard Gaussian entries, Edelman (1988) showed that , so random matrices are only mildly ill-conditioned. Trefethen and Schreiber's experiments (1990) found typical growth factors of order . Smoothed analysis (Spielman and Teng, 2004) studies the worst case over data perturbed by small random noise. Sankar, Spielman and Teng (2006) showed that for Gaussian elimination without pivoting the growth stays polynomial in and with high probability, where is the noise level. A smoothed analysis that fully explains partial pivoting is still a research question.
Computation— Floating point, complexity and the cost of linear algebra
The IEEE 754 standard (1985), designed largely by William Kahan, fixed the model used here: correctly rounded basic operations, so that is a guarantee, not an approximation. It also defined infinities, NaNs and gradual underflow. Its predictability is what makes backward error analysis possible. On the complexity side, Gaussian elimination costs , but Volker Strassen (1969) showed that matrix multiplication, and therefore solving linear systems and inversion, can be done in . The exponent of matrix multiplication has been below since Coppersmith and Winograd (1990), with small further improvements since 2010. Whether is open, and these fast algorithms raise their own questions of numerical stability. See computational complexity.
History and global context
- Elimination. The Nine Chapters on the Mathematical Art (China, compiled by about the 1st century CE) solves systems of linear equations by the fangcheng method: elimination on a counting board, column by column, with negative numbers, essentially Gaussian elimination. Carl Friedrich Gauss used elimination systematically for least-squares problems in astronomy and geodesy (1809–1810), after Adrien-Marie Legendre published the method of least squares in 1805.
- ODE methods. Leonhard Euler described his method in 1768. Carl Runge (1895), Karl Heun (1900) and Martin Wilhelm Kutta (1901) developed the methods named after them, and John Butcher's algebraic theory of order conditions (from 1963) made Runge–Kutta methods a subject in their own right.
- The computer age. John von Neumann and Herman Goldstine's 1947 analysis of matrix inversion, and Alan Turing's 1948 paper "Rounding-off errors in matrix processes" (which introduced the term condition number and the LU language), founded rounding-error analysis. James Wilkinson developed backward error analysis at the National Physical Laboratory (Rounding Errors in Algebraic Processes, 1963; The Algebraic Eigenvalue Problem, 1965). Alston Householder introduced his reflections in 1958.
- Eigenvalues. The QR algorithm was discovered independently in 1961 by John Francis in England and Vera Kublanovskaya in Leningrad. It is often listed among the most important algorithms of the twentieth century.
- Stiffness and stability. Charles Curtiss and Joseph Hirschfelder named stiffness in 1952, in chemical kinetics. Germund Dahlquist in Stockholm proved his stability barriers (1956, 1963), and Peter Lax and Robert Richtmyer their equivalence theorem (1956).
- Floating point. William Kahan led the design of IEEE 754 (1985), for which he received the Turing Award in 1989.
Further reading
- Lloyd N. Trefethen and David Bau III, Numerical Linear Algebra (1997) — forty short lectures on conditioning, stability, QR, least squares and eigenvalue algorithms.
- Endre Süli and David F. Mayers, An Introduction to Numerical Analysis (2003) — a rigorous general course, including ODE methods and their convergence proofs.
- Nicholas J. Higham, Accuracy and Stability of Numerical Algorithms (2nd edition, 2002) — the reference for rounding-error analysis, including Wilkinson's results.
- Ernst Hairer, Syvert P. Nørsett and Gerhard Wanner, Solving Ordinary Differential Equations I (1987), and Hairer and Wanner, II: Stiff and Differential-Algebraic Problems (1991) — order conditions, stability and stiffness in depth.