Errors, range and precision

Rounding errors, absolute and relative error, range against precision, overflow and underflow, with a real sensor reading.

A7.4Data representationA level30 min

Covers OCR H446 1.4.1 Data Types · AQA 7517 4.5.4.5 Rounding errors · AQA 7517 4.5.4.6 Absolute and relative errors · AQA 7517 4.5.4.7 Range and precision · AQA 7517 4.5.4.9 Underflow and overflow · Eduqas A500QS 2.3 Data representation and data types

Do this lesson in the simulator

A 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...

0.1 in binary is 0.0 followed by 0011 repeated for ever; a byte keeps only the first 8 bits, 00011001, which is 25/2560.00011001100110011...0011001100110011the block 0011 repeats for evera byte keeps these 8cut offstored: 0.00011001 = 25/256 = 0.09765625lost with the cut-off bits: 0.00234375, the rounding error
However many bits you keep, the pattern 0011 carries on past them, so 0.1 can never be stored exactly. One byte keeps 00011001, which is 25/256, a little under 0.1.
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)

Run this in the simulator

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%).

An error of 1 mm is 0.02% of a 5 m corridor but 50% of a 2 mm gapa 5 m corridor: 1 mm out of 5,000 mmrelative error 0.02%the 1 mm error is this thin linea 2 mm gap: 1 mm out of 2 mmrelative error 50%
The absolute error is 1 mm both times. Divided by the true value it becomes 0.02% of the corridor, which nobody would notice, and half of the gap, which is useless. That is why relative error is usually the more useful measure.

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.

The values each format can hold between 1 and 2, 10 and 11, and 100 and 1011 to 210 to 11100 to 101fixed, 4 + 416 values16 valuesout of rangefixed, 6 + 24 values4 valuesout of rangefloating point64 values8 values1 valueeach tick is one value the format can store exactly
Fixed point has the same gaps everywhere, and simply stops at its largest value. Floating point, with an 8-bit mantissa and 4-bit exponent, packs 64 values between 1 and 2 but only one between 100 and 101: its precision is relative to the size of the number.
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.

The number line for an 8-bit mantissa and 4-bit exponent: overflow beyond -128 and 127, underflow in the gap either side of 0-1281270-0.001980.001953125overflowoverflowunderflowunderflow: too close to 0 to store, so it becomes 0can be storedcan be storednot to scale: the gap round 0 is really far narrower
Two kinds of too far. A result beyond 127 or below -128 needs a bigger exponent than 4 bits can hold: overflow. A result closer to 0 than the smallest normalised value needs a more negative one: underflow.
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

Run this in the simulator

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 float
  • stored: <bits>, the stored whole number as 8 binary digits (you may use format(q, "08b"))
  • stored value: <value> m, the stored whole number divided by 256
  • absolute error: <error> m, the absolute error, then round(error, 7)
  • relative error: <percent>%, the relative error as a percentage, then round(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

  1. Round instead of truncating. Does the absolute error go down for this reading? Is that true for every reading?
  2. Use 16 bits with 8 after the point. What are the new range and precision?
  3. Find the largest reading this byte format can hold, and what happens to a reading of 1.2 m.