Errors, range and precision
Rounding errors, absolute and relative error, range against precision, overflow and underflow, with a real sensor reading.
Do this lesson in the simulatorA fixed number of bits can only hold a fixed number of different values, but between any two real numbers there are infinitely many more. So most real numbers cannot be stored exactly. This lesson measures how wrong a stored value is, shows how the choice of format trades range against precision, and names what happens when a result is too big or too small to store at all.
Rounding errors
Some fractions that are simple in denary have no exact binary form. 0.1 is 1/10, and 10 has a factor of 5, so in binary 0.1 repeats for ever: 0.000110011001100... Cut it off after any number of bits and what is stored is slightly wrong. The same thing happens in denary with 1/3 = 0.333...
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(abs((0.1 + 0.2) - 0.3) < 1e-9) # compare reals with a tolerance, never with ==
total = 0.0
for i in range(10):
total = total + 0.1
print(total)
Each 0.1 is stored a little wrong, and adding them lets the errors build up. This is a rounding error: the difference between a value and the nearest one the format can hold. The rule for programs follows from it: never test two reals for exact equality. Test whether they are within a small tolerance, or store money as whole pennies.
Absolute and relative errors
Two ways to say how big an error is:
- Absolute error = |true value - stored value|. It has the same units as the value.
- Relative error = absolute error ÷ |true value|. It has no units, and is often given as a percentage.
Store 0.1 in a byte of unsigned fixed point with all 8 bits after the point. The byte holds a whole number of 256ths, and 0.1 × 256 = 25.6, so truncating stores 25:
| Value | |
|---|---|
| true value | 0.1 |
| stored, 25/256 | 0.09765625 |
| absolute error | 0.00234375 |
| relative error | 0.00234375 ÷ 0.1 = 0.0234375, about 2.34% |
Relative error is usually the more useful one. An absolute error of 1 mm is nothing on a 5 m corridor (0.02%) and ruinous on a 2 mm gap (50%).
Range and precision
Range is the span from the smallest to the largest value a format can hold. Precision is how close together the values it can hold are, which decides how accurately a value can be stored.
With a fixed total number of bits, you trade one against the other.
Fixed point. In 8 unsigned bits, 4 whole and 4 fraction bits give a range of 0 to 15.9375 with a precision of 1/16 = 0.0625. Moving the point to 6 whole and 2 fraction bits gives 0 to 63.75, but now the steps are 0.25. The precision is the same at every size of number.
Floating point. With 12 bits, an 8-bit mantissa and a 4-bit exponent:
- largest value: 0.1111111 × 2⁷ = 127
- most negative value: 1.0000000 × 2⁷ = -128
- smallest positive normalised value: 0.1000000 × 2⁻⁸ = 0.001953125
More mantissa bits give more precision; more exponent bits give more range. Floating point spreads its precision: small numbers are stored with small gaps between them, large numbers with large gaps.
| Fixed point | Floating point | |
|---|---|---|
| Range for the same bits | smaller | much larger |
| Precision | constant, the same for every value | relative: fine for small numbers, coarse for large |
| Speed | fast, integer arithmetic | slower, needs normalising, often dedicated hardware |
| Good for | money in pennies, sensor readings with a known range | scientific values that vary hugely in size |
Overflow and underflow
Overflow is a result too large for the format. In integers the bits wrap round (lesson A7.2). In floating point the exponent needed is bigger than the exponent field can hold.
Underflow is a result too close to zero for the format: its exponent would be more negative than the smallest exponent. The format cannot store it, so it becomes zero, and any later division by it fails.
big = 1e308
print(big * 10) # overflow: Python's float becomes inf
tiny = 5e-324 # the smallest positive float Python has
print(tiny / 2) # underflow: rounds to 0.0
Both are silent in many languages, which makes them dangerous: a speed that overflows to a huge negative number, or a time step that underflows to zero, gives wrong results with no error message.
A real reading
The robot's distance sensor reads to 0.1 cm. Suppose the reading must go into one byte of fixed point, in metres, with all 8 bits after the point (so up to 0.99609375 m, in steps of 1/256 m). Precision is fixed at 1/256 m, about 0.39 cm, so every reading suffers a rounding error of up to that much.
Task: store a reading
The robot faces a wall. Read distance() once (it returns centimetres) and convert it to metres. Store the metres in a byte of unsigned fixed point with all 8 bits after the point: the stored whole number is int(metres * 256), which truncates. Then print exactly these five lines, calculating every value:
reading: <metres> m, the metres as Python prints the floatstored: <bits>, the stored whole number as 8 binary digits (you may useformat(q, "08b"))stored value: <value> m, the stored whole number divided by 256absolute error: <error> m, the absolute error, thenround(error, 7)relative error: <percent>%, the relative error as a percentage, thenround(percent, 2), with no space before the%
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
metres = distance() / 100
print("reading:", metres, "m")
Challenges
- Round instead of truncating. Does the absolute error go down for this reading? Is that true for every reading?
- Use 16 bits with 8 after the point. What are the new range and precision?
- Find the largest reading this byte format can hold, and what happens to a reading of 1.2 m.