reference <- 6.239
approximation <- 6.24
absolute_error <- abs(approximation - reference)
relative_error <- absolute_error / abs(reference)
absolute_error[1] 0.001
100 * relative_error # percent[1] 0.01602821
Module 5.2 · Errors
Our R code can run without an error message and still give a bad answer. Where can an error enter a simulation, how can we measure it, and when should it change our conclusion?
Download the complete R script after coding along. Read Module 5.2, “Errors,” in Introduction to Computational Science. This is one textbook module; we will work through it over two class meetings. Today we focus on sources of error, measuring error, and floating-point arithmetic. The continuation is ready below.
First, watch ESA’s 4½-minute footage of the Ariane 501 launch failure. It shows the event but does not explain the cause. What evidence would we need to find out what went wrong?
Then watch Engineering Disasters 13 — Software Flaws, the excerpt about Ariane 5 and the Patriot missile system. As you watch, note what went wrong in each case and whether the two failures had the same cause.
NASA’s Mars Climate Orbiter was lost after ground software used English units while onboard software used metric units. Before we discuss any floating-point arithmetic, ask: Would a more precise computer have fixed that problem? Why or why not?
Errors can enter at different stages:
| Type | Example | What to check |
|---|---|---|
| Data | An instrument reports the wrong temperature | Calibration and source quality |
| Model | We omit an important way a disease spreads | Assumptions and validation against observations |
| Implementation | One program supplies pounds while another expects newtons | Code, units, and interface tests |
| Numerical | A finite time step approximates continuous change | Precision and sensitivity to the method or step size |
The last category is our main coding focus, but fixing numerical error cannot repair a wrong model or incorrect units.
Suppose the reference value is \(6.239\), but we report \(6.24\). Predict the difference before running R.
\[ \text{absolute error}=|\text{approximation}-\text{reference}|, \qquad \text{relative error}=\frac{\text{absolute error}}{|\text{reference}|}. \]
The relative error is undefined when the reference value is zero. Multiply it by 100 to express it as a percentage.
[1] 0.001
[1] 0.01602821
Which measure is easier to interpret if the number represents meters? Which helps compare errors for values measured on very different scales?
In a real modeling problem, we often do not know the true answer. We can compare against a measurement, an analytical solution, or a more refined computation, but must say which reference we used. A small difference from one reference is not proof that our model describes reality.
0.1 + 0.2 surprise us?Predict whether the final line is TRUE or FALSE:
R prints a rounded display. Ask it to show more digits:
[1] "0.30000000000000004"
[1] "0.29999999999999999"
[1] 5.551115e-17
Most decimal fractions cannot be represented exactly with a finite number of binary digits. The computer stores nearby values; the difference here is tiny, but exact equality sees it.
When our question is whether two computed values are close enough, choose a tolerance appropriate to the scale of the problem:
This tolerance is suitable for this small arithmetic example; it is not a universal threshold for every model or unit. R also provides all.equal() for approximate comparisons.
Try these two expressions before reading the output. In exact arithmetic, both are zero. R evaluates each one from left to right:
On our R setup, the first leaves a tiny negative remainder (about \(-3.6\times10^{-16}\)), while the second gives exactly zero. The intermediate calculations round differently, even though the expressions are algebraically equivalent. Which error measure can we use when the correct answer is zero: absolute or relative error? Would either result matter if these were measurements in meters?
Our simulations have used a vector of times made with seq(). What might happen if we repeatedly add \(0.1\) instead? Predict whether both methods below give exactly 10,000.
[1] 10000
[1] 10000
[1] 1.884837e-08
The accumulated difference is small here. The general lesson is to measure the effect, not to assume every floating-point difference matters. When time is determined by the step number, calculating it from that number avoids repeated addition of the time step.
Classify each as data, model, implementation, or numerical error:
Which problem could be reduced by making the time step smaller? Which ones could not?
Today we will read numbers the way Module 5.2 does, then make three predictions and test them in R: Can a number be too large or too small? Does arithmetic order matter? What changes when an Euler simulation uses a smaller time step?
First, recall yesterday’s distinction: a wrong unit or a missing biological mechanism will not be repaired by a more precise calculation. Today’s examples focus on what the computation itself can change.
The book uses normalized exponential notation with the decimal point before the first nonzero digit. This differs from the scientific notation many of us learned, which puts one nonzero digit before the decimal point. Both notations name the same value:
\[ 0.0004500 = 0.4500\times10^{-3} = 4.500\times10^{-4}. \]
The middle expression is normalized in the book’s convention. In \(0.4500\times10^{-3}\), the book calls 4500 the significand (the digits after dropping the decimal point), precision is 4 significant digits, and magnitude is \(10^{-3}\)—not merely the exponent \(-3\). The leading zeros do not count; the final two zeros after the decimal point do.
Try one together without R: write \(3{,}704{,}000\) in the book’s normalized notation. Which digits count as significant? What are its precision and magnitude? The book’s convention treats trailing zeros in an integer written without a decimal point as not significant, so the answer is \(0.3704\times10^7\), precision 4, magnitude \(10^7\).
For \(0.09200\), predict the book’s normalized form, precision, and magnitude before looking: \(0.9200\times10^{-1}\), precision 4, magnitude \(10^{-1}\). If R prints 0.092, it has displayed the numeric value but not the intended trailing zeros in the written measurement.
These definitions help us discuss how many digits a stored or reported result carries. They do not tell us whether a model is scientifically accurate.
A computer stores numbers within a finite range. Predict the two results:
In ordinary R numeric arithmetic, the first becomes Inf (too large) and the second becomes 0 (too small to represent here). Both should trigger questions if they appear unexpectedly in a simulation. In ESA’s account of Ariane 5 Flight 501, a value exceeded the capacity of a converted representation; that was not the same mechanism as 0.1 + 0.2.
Our \(9.1\) subtraction produced a tiny discrepancy. Mathematically, \((a+b)+c=a+(b+c)\) too—but finite precision can make the difference much larger. Predict these R results:
Why can the 1 be lost in one order? This is another consequence of finite precision. The textbook also discusses adding small quantities before large ones when feasible.
Return to the growth model from Module 2.2: the starting population is 100, and its growth rate is 10% of its current size per hour. How many individuals does a one-hour Euler step add at the start? What would the first half-hour step add instead?
The first one-hour step adds \(1(0.1)(100)=10\), reaching 110. The first half-hour step adds \(0.5(0.1)(100)=5\), reaching 105. After that half hour, the rate must be calculated again from 105, not 100. Smaller steps let the rate change more often.
Code the familiar loop with a one-hour step. The exact value at hour 8 is available for this particular growth model, so we can measure the numerical error.
delta_t <- 1
times <- seq(0, 8, by = delta_t)
population <- numeric(length(times))
population[1] <- 100
for (i in 2:length(times)) {
old_population <- population[i - 1]
growth_rate <- 0.1 * old_population
population[i] <- old_population + delta_t * growth_rate
}
one_hour_result <- population[length(population)]
exact_result <- 100 * exp(0.1 * 8)
one_hour_result[1] 214.3589
[1] 222.5541
[1] 8.195212
Now change only the time step to half an hour. Predict whether the result will move closer to or farther from the exact value before running this chunk.
delta_t <- 0.5
times <- seq(0, 8, by = delta_t)
population <- numeric(length(times))
population[1] <- 100
for (i in 2:length(times)) {
old_population <- population[i - 1]
growth_rate <- 0.1 * old_population
population[i] <- old_population + delta_t * growth_rate
}
half_hour_result <- population[length(population)]
half_hour_result[1] 218.2875
[1] 4.266634
The half-hour result is closer here. If time permits, change delta_t to 0.1 and then 0.01 in that second chunk. Does the answer appear to settle down? A smaller step can reduce time-step approximation error, but it cannot fix a wrong growth assumption or incorrect data. It also does not make floating-point roundoff disappear.
If time permits, connect this to the book: Module 5.2 demonstrates truncation error by stopping an infinite series after finitely many terms, such as \(e^x=1+x+x^2/2!+\cdots\). A finite Euler step is another approximation of continuous mathematics. Do not confuse either with round-off error, which comes from finite storage of numbers.
For one of our growth, predator–prey, or SIR simulations, propose three checks. Consider units, conservation or other invariants, a different time step, and whether the assumptions fit the question. What would each check reveal—and what could it not reveal?
Before leaving: If the half-hour answer is closer to the exact solution, have we shown that the growth model accurately predicts a real population? Explain in one sentence.
“The code ran” is only a starting point. Name the source of possible error, estimate its size when possible, and decide whether it changes the conclusion you are willing to draw.