0.1 + 0.20.30000000000000004
After completing this unit, you will be able to:
Local prerequisites: arithmetic with rational numbers, functions, simple equations, and reasoning about “for every” versus “there exists.” No calculus or linear algebra is required.
Consider the rational-number identity
There are three distinct layers:
A common mistake is to treat the output as the object itself. A program that prints 0.30000000000000004 does not refute the rational-number identity above. It reveals a property of the floating-point representation used by the program.
Python provides floating-point numbers for numerical computation and Fraction for exact rational numbers.
0.1 + 0.20.30000000000000004
from fractions import Fraction
Fraction(1, 10) + Fraction(2, 10)Fraction(3, 10)
Different representations serve different needs. Exact arithmetic preserves algebraic equalities, but numerators and denominators can grow in size. Floating-point arithmetic is fast and underlies many numerical libraries, but introduces rounding error.
Numerical comparisons must therefore specify both tolerance and scale:
from math import isclose
x = 0.1 + 0.2
isclose(x, 0.3, rel_tol=1e-12, abs_tol=1e-15)True
A tolerance is not a magic formula. It is part of the experiment specification. We should be able to explain why its magnitude is appropriate for the question being asked.
A computational experiment needs at least the following seven elements.
| Element | Question to answer |
|---|---|
| Question | What claim or phenomenon are we investigating? |
| Environment | Which language and software versions are used? |
| Inputs | What are the data, parameters, and their units? |
| Method | Which algorithm and representation are used? |
| Checks | Which tests can detect implementation errors? |
| Outputs | What raw data and summaries are saved? |
| Conclusion | What do the results support, and what remains unproved? |
Run the Unit 1 experiment from the project root:
python source/code/unit01_experiment.py
The resulting JSON file records the Python version, parameters, exact result, floating-point result, and checks of two conjectures. Running the same command in the same environment should produce identical bytes.
An experiment can check that
is even for the first thousand integers tested. This check provides useful computational evidence: it can uncover code errors or counterexamples. However, a thousand cases do not prove the claim for every integer.
A short general proof is
Of any two consecutive integers, one is even, so their product is even. This argument applies to every integer, not just the inputs we happened to run.
By contrast, a single counterexample is enough to refute a universal claim. The polynomial
produces primes for many small values, but p(40)=41². A computer can find this example; the factorization explains mathematically why the claim “prime for every nonnegative integer” is false.
A program test makes a bounded, checkable claim, for example:
3/10;0.3 at the specified tolerance;n=40; andTests increase confidence in an implementation. They do not, by themselves, prove the theorem being implemented.
Explain in your own words why the output 0.30000000000000004 does not make the equation false.
Separate the mathematical object from the way the computer stores it.
The equality is an exact equality of rational numbers. The numbers 0.1 and 0.2 are not stored exactly in the binary floating-point representation Python uses. The small error in the output comes from the approximate representation, not from a change in the rules of rational addition.
Describe one situation where abs_tol is appropriate and one where rel_tol is more appropriate. Give reasons, not just numbers.
Compare quantities close to zero with quantities that have a large natural scale.
For an equation residual that is theoretically zero, an absolute tolerance is appropriate because the error is compared directly with zero. When comparing two distances of roughly a million meters, a relative tolerance is more informative because the acceptable error scales with the distance. In practice, both are often used together and must be derived from the problem’s requirements.
Change the parity-check limit from 1,000 to 100,000. What new conclusion is justified, and what conclusion remains unjustified?
More cases enlarge the region that has been checked; they do not turn it into the set of all integers.
Justified: no counterexample was found in the range 0 through 99,999, assuming the checker is implemented correctly. Unjustified: the claim has been proved for every integer. A general proof still requires an argument such as the factorization n(n+1).
Use the program to find the smallest nonnegative integer for which is composite. Then explain the result without relying on the program.
Check 40 and factor the result.
The smallest value is n=40. Exactly, , so the value is composite. This exact calculation provides a mathematical explanation of compositeness that can be checked independently of the program. Establishing that 40 is the smallest such value additionally requires exact primality checks for every integer from 0 through 39; the factorization alone does not establish minimality.
Read output/unit01-results.json. Name two fields needed to rerun the experiment and one additional piece of information that would be needed if the experiment used external libraries.
Look at the parameters and environment.
One possible answer: limit and the Python version are needed. If the experiment uses external libraries, the record must also include their names and versions, and ideally an environment file or dependency lockfile from which they can be reinstalled.