5.4.22 · D3Scientific Computing (Python)

Worked examples — Floating point gotchas — catastrophic cancellation, associativity failure

2,853 words13 min readBack to topic

Before anything: three words we will reuse.


The scenario matrix

Every trouble on this topic falls into one of these cells. Each row is one worked example below.

# Case class Trigger What breaks Fix family
A Subtract near-equal positives , both catastrophic cancellation algebraic reformulation
B Quadratic, same-sign root , take safe sign fine (no fix needed)
C Quadratic, opposite-sign root the other root cancellation in
D Degenerate: term rounds to zero , total loss (answer = 0) trig identity
E Absorption — small + huge the small one vanishes reorder / Kahan
F Associativity flip grouping changes answer cancel large terms first
G Summation order over a list many tiny + one huge error grows with sort ascending / Kahan
H Equality comparison 0.1+0.2 == 0.3 False unexpectedly tolerance compare
I Real-world word problem GPS / distance difference cancellation hides in disguise reformulate the physics
J Exam twist "higher precision fixes it?" conditioning ≠ precision reformulate, not upgrade

Example A — subtract near-equal positives

Steps.

  1. True value. . Why this step? We need the honest target so we can measure error against it. The approximation comes from .

  2. Naive compute. In doubles, exactly, and — but rounds to (the gap near is , so adding falls off the edge, cell E in disguise). So both square roots equal , and the subtraction gives . Why this step? Shows the double failure: absorption inside, then subtracting two identical numbers gives exactly .

  3. Measure the damage. Relative error — 100% wrong. Why this step? Confirms "catastrophic": the answer is entirely lost.

  4. Reformulate. Multiply top and bottom by the conjugate: Now compute . No subtraction of near-equals — we only add same-sign roots and divide. Why this step? Division and same-sign addition never amplify relative error (parent note, §2), so we route around the villain.

Verify: the reformulated value matches the true to full precision. Sanity: for large the gap between consecutive roots should shrink like — it does. ✓


Examples B & C — the two quadratic roots

We take one quadratic and extract both roots, because the two roots live in different matrix cells: one is safe (B), one cancels (C).

Steps.

  1. Discriminant. , so . Why this step? Both roots share this square root; compute once.

  2. Same-sign root (Cell B — safe). Since , the term is positive; the safe root adds two positives: Why this step? Adding two same-sign near-equal numbers does not cancel — no amplification. This root is trustworthy.

  3. Opposite-sign root, naive (Cell C — catastrophic). Here the cancellation happens between two operands and . The parent note's amplifier, written for a generic subtraction , is (the letters in §2 of the parent are just placeholder operands — not the quadratic coefficients here). Plugging in: — precision annihilated. Why this step? Exposes the cancellation and its size, being careful not to confuse the operands with the coefficients .

  4. Stable second root. Use (product of roots), so Why this step? We already have the safe root ; the product identity gives using only division — no subtraction of near-equals. See Quadratic formula numerical issues.

Verify: plug back: . Product check: . Sum check: . ✓


Example D — a term rounds to zero (degenerate)

Steps.

  1. True value. Taylor: , so and . Why this step? Establishes the target .

  2. What the machine stores. . But , so rounds to exactly . Why this step? The information that made nonzero lives below machine epsilon — it is gone before subtraction.

  3. Naive result. , so . Relative error (total loss). Why this step? Confirms degeneracy: the answer collapses to zero.

  4. Reformulate with a trig identity. , hence of a small argument is computed accurately (it's the argument, no cancellation), and , so . Why this step? We replaced "subtract near-equals" by " of a small number", which the library evaluates faithfully.

Verify: at , , so . Matches the limit. ✓

The figure below plots both the naive and reformulated versions across many . Read it like this: the dashed gray line is the true limit ; the green curve (reformulated) hugs it all the way down; the red curve (naive) rides along until , then falls off a cliff to the moment rounds to . That cliff is the catastrophic cancellation — the picture shows exactly where in the naive formula dies.

Figure — Floating point gotchas — catastrophic cancellation, associativity failure

Example E — absorption (small + huge)

Steps.

  1. Gap at . Near a value of magnitude , consecutive doubles differ by (the mantissa's last bit now weighs , not ). So has neighbours apart. Why this step? We must know the resolution at that magnitude — precision is relative, so big numbers have coarse spacing (see IEEE-754 floating point representation).

  2. Add the 1. (odd) — but that's not representable; the nearest doubles are and . Rounding-to-nearest keeps . Why this step? The is smaller than half the gap, so it is absorbed — vanishes entirely.

  3. Consequence. . The 1 left no trace. Why this step? This is the mechanism behind the associativity break in Example F.

Verify: in Python, 1e16 + 1 == 1e16 is True; 1e16 + 2 == 1e16 is False. The gap is exactly . ✓


Example F — associativity flip

Before the example, one small lemma we will lean on.

Steps.

  1. Left grouping . (absorption, Example E). Then . Why this step? Shows the 1 dies in the first add, so the result is .

  2. Right grouping . exactly — this is the Sterbenz lemma above: and are equal in magnitude, so the subtraction introduces no rounding. Then . Why this step? Here the huge numbers cancel first (exactly), protecting the tiny .

  3. Compare. Left , right . Differ by 100%. Why this step? Demonstrates floating addition is not associative — grouping decides which bits survive.

  4. Rule of thumb. Cancel large opposite-sign terms before adding small ones. Why this step? Prevents the small term being absorbed.

Verify: (1.0 + 1e16) + (-1e16) == 0.0 and 1.0 + (1e16 + (-1e16)) == 1.0. ✓

The bar chart below makes the flip vivid. Read it like this: the left red bar is the grouping — it collapses to because the was absorbed by before could cancel it; the right green bar is — it reaches because the two huge terms annihilate first (Sterbenz-exact) and the survives. Two identical sets of numbers, two different heights: that height gap is associativity failure.

Figure — Floating point gotchas — catastrophic cancellation, associativity failure

Example G — summation order over a list

Steps.

  1. Naive left-to-right. Start . Add the first : (absorbed). Every subsequent is likewise absorbed. Final — all ones lost. Why this step? The running partial sum is huge from step one, so each tiny addend falls below its gap.

  2. Ascending order. Sort so the huge number is last: sum the million ones first, getting (each add is between comparable numbers — safe), then add . Now : the gap at is , and , so it survives: . Why this step? Adding smallest first lets them accumulate into a number big enough not to be absorbed. See Round-off error propagation.

  3. Kahan option. Kahan compensated summation carries the lost low bits in a compensator c, giving error independent of order and of . Why this step? When you can't sort (streaming data), Kahan recovers accuracy anyway.

Verify: naive gives ; ascending gives ; both compared to the true . ✓


Example H — the equality trap

Steps.

  1. Why they aren't exact. are not finite binary fractions (like in decimal). Each is stored as the nearest double, already off by . Why this step? The premise "equal in math ⇒ equal in floats" is false because the inputs themselves are rounded.

  2. The sum. , while . They differ by . Why this step? Shows the two sides land on different doubles, so == returns False.

  3. Fix: tolerance compare. Use math.isclose(x, y), i.e. with default rtol=1e-9. Why this step? We ask "are they equal within round-off?" instead of "bit-identical?".

Verify: 0.1 + 0.2 == 0.3 is False; the difference is about ; abs((0.1+0.2)-0.3) < 1e-9 is True. ✓


Example I — real-world word problem

Steps.

  1. True difference. . Why this step? Sets the target: a 25 cm difference.

  2. Digit accounting. Each altitude is good to significant digits (double). Absolute error of each is about . Subtraction is exact (Sterbenz — the two values are within a factor of 2), so those two absolute errors carry straight into : worst case . Relative error of is then . Why this step? The relative-error amplifier tells us we lose about digits. Starting from good digits, roughly remain — so is still fine here. But if the two altitudes were 25 nanometres apart, the amplifier would be and nothing would survive. Why this matters: the danger scales with how close the two big numbers are.

  3. Engineering fix. Store the difference from a common reference: subtract a base altitude (say ) from each reading before rounding, or work in a local coordinate frame where the small quantity is represented directly rather than as a difference of two huge ones. Then you keep full precision on the 25 cm quantity, because it is never born from a big-minus-big. Why this step? Never let the interesting small number arise as a difference of two nearly-equal large numbers — represent it directly. See Numerical stability and conditioning.

Verify: ; amplifier ; digits lost . ✓


Example J — the exam twist

Steps.

  1. What float128 buys. `np.float128` (x86 80-bit extended) has significant digits, . Why this step? Know the new resolution before claiming it helps.

  2. Does the cancellation vanish? ; now , so the term survives at this particular , and you get . It looks fixed. Why this step? Shows higher precision only postpones the cliff.

  3. Push smaller. At , , so float128 also rounds to . The catastrophe returns — you only moved the threshold. Why this step? Proves the problem is the formula's conditioning, not the machine's precision.

  4. Correct fix. Reformulate as (Example D). This works in plain float64 for all small . Why this step? Fixing the expression beats upgrading the hardware — the mistake "more precision always fixes accuracy" from the parent note, cell J.

Verify: at , float64 naive but reformulated ; the reformulation needs no extra precision. ✓


Recall

Recall Cover the answers — which fix for which cell?

Which cell is "subtract two big same-sign numbers"? ::: A / C — catastrophic cancellation, fix by reformulation. The safe quadratic root uses which sign of ? ::: The sign that makes add (same sign as ), so no cancellation. How do you get the dangerous quadratic root safely? ::: from the product of roots. Why does give exactly 0 in float64? ::: The correction is below , so rounds to . Best summation order for one-huge-many-tiny? ::: Ascending (smallest first) — or Kahan. Does float128 fix a cancelling formula? ::: No, it only moves the threshold; reformulate instead. What does the Sterbenz lemma guarantee? ::: If two same-sign floats are within a factor of 2, their subtraction is computed exactly (no rounding).