15. Floating Point Arithmetic:  Issues and Limitations
******************************************************

Floating-point numbers are represented in computer hardware as base 2
(binary) fractions.  For example, the **decimal** fraction "0.125" has
value 1/10 + 2/100 + 5/1000, and in the same way the **binary**
fraction "0.001" has value 0/2 + 0/4 + 1/8. These two fractions have
identical values, the only real difference being that the first is
written in base 10 fractional notation, and the second in base 2.

Din păcate, cele mai multe fracții zecimale nu pot fi reprezentate
exact sub formă de fracții binare. O consecință a acestui fapt este
că, în general, numerele zecimale în virgulă mobilă pe care le tastați
sunt doar aproximații ale numerelor binare în virgulă mobilă care vor
fi stocate în mașina de calcul.

Dificultatea este mai ușor de înțeles dacă o abordăm pornind din baza
10. Să luăm în considerare fracția 1/3. O putem aproxima ca fracție în
baza 10

   0.3

sau, și mai bine,

   0.33

sau, și mai bine,

   0.333

șamd. Oricât de multe cifre ne-am strădui să tastăm, rezultatul nu va
fi niciodată exact 1/3, chiar dacă vom ajunge la aproximații tot mai
bune ale lui 1/3.

La fel, oricât de multe cifre în baza de numerație 2 ne-am strădui să
întrebuințăm, valoarea zecimală 0.1 nu va putea fi reprezentată exact
ca fracție în baza 2. Pentru că, în baza 2, 1/10 este fracția
(periodică mixtă) cu număr infinit de cifre la dreapta punctului

   0.0001100110011001100110011001100110011001100110011...

Dacă de oprim, din tastat, după indiferent câți biți (cifre binare),
tot ce obținem este o aproximație. Pe majoritatea mașinilor de calcul
din zilele noastre, numerele în virgulă mobilă (în englezește, ca
jargon, ele sunt denumite *float*-uri) sunt aproximate folosind o
fracție binară al cărei numărător utilizează primii 53 de biți,
începând cu bitul cel mai semnificativ, iar al cărei numitor este o
putere a lui 2.  În cazul lui 1/10, fracția binară este
"3602879701896397 / 2 ** 55", adică foarte aproape de valoarea exactă
a lui 1/10 însă nu chiar egală cu ea.

Many users are not aware of the approximation because of the way
values are displayed.  Python only prints a decimal approximation to
the true decimal value of the binary approximation stored by the
machine.  On most machines, if Python were to print the true decimal
value of the binary approximation stored for 0.1, it would have to
display

   >>> 0.1
   0.1000000000000000055511151231257827021181583404541015625

That is more digits than most people find useful, so Python keeps the
number of digits manageable by displaying a rounded value instead

   >>> 1 / 10
   0.1

Să reținem, așadar, că, în pofida faptului că rezultatul afișat arată
la fel ca valoarea exactă a lui 1/10, valoarea efectiv stocată este
cea mai apropiată (de rezultat) fracție binară pe care mașina de
calcul o poate reprezenta.

Ca o curiozitate, există mai multe numere zecimale (așadar, diferite)
cu aceeași cea mai bună aproximație printr-o fracție binară. De
exemplu, numerele "0.1" și "0.10000000000000001", precum și
"0.1000000000000000055511151231257827021181583404541015625" sunt cu
toatele aproximate de "3602879701896397 / 2 ** 55". Dat fiind că toate
aceste valori zecimale au aceeași aproximație, oricare din ele ar
putea fi afișată, atunci când se cere, păstrându-se invariantul
"eval(repr(x)) == x".

În trecut, funcția predefinită a Python-ului "repr()" ar fi ales
numărul cu 17 cifre semnificative, și anume pe "0.10000000000000001".
În zilele noastre, începând cu Python 3.1, Python-ul este capabil (pe
majoritatea mașinilor de calcul)  să aleagă numărul cel mai scurt și
să afișeze, pur și simplu, "0.1".

Note that this is in the very nature of binary floating-point: this is
not a bug in Python, and it is not a bug in your code either.  You'll
see the same kind of thing in all languages that support your
hardware's floating-point arithmetic (although some languages may not
*display* the difference by default, or in all output modes).

For more pleasant output, you may wish to use string formatting to
produce a limited number of significant digits:

   >>> format(math.pi, '.12g')  # give 12 significant digits
   '3.14159265359'

   >>> format(math.pi, '.2f')   # give 2 digits after the point
   '3.14'

   >>> repr(math.pi)
   '3.141592653589793'

Merită să înțelegem că aceasta este, de fapt, o iluzie: noi nu facem
decât să rotunjim *afișajul* valorii stocate în mașina de calcul.

One illusion may beget another.  For example, since 0.1 is not exactly
1/10, summing three values of 0.1 may not yield exactly 0.3, either:

   >>> .1 + .1 + .1 == .3
   False

Also, since the 0.1 cannot get any closer to the exact value of 1/10
and 0.3 cannot get any closer to the exact value of 3/10, then pre-
rounding with "round()" function cannot help:

   >>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
   False

Though the numbers cannot be made closer to their intended exact
values, the "round()" function can be useful for post-rounding so that
results with inexact values become comparable to one another:

   >>> round(.1 + .1 + .1, 10) == round(.3, 10)
   True

Binary floating-point arithmetic holds many surprises like this.  The
problem with "0.1" is explained in precise detail below, in the
"Representation Error" section.  See Examples of Floating Point
Problems for a pleasant summary of how binary floating-point works and
the kinds of problems commonly encountered in practice.  Also see The
Perils of Floating Point for a more complete account of other common
surprises.

As that says near the end, "there are no easy answers."  Still, don't
be unduly wary of floating-point!  The errors in Python float
operations are inherited from the floating-point hardware, and on most
machines are on the order of no more than 1 part in 2**53 per
operation.  That's more than adequate for most tasks, but you do need
to keep in mind that it's not decimal arithmetic and that every float
operation can suffer a new rounding error.

Deși cazurile patologice vor continua să existe, utilizările obișnuite
ale aritmeticii în virgulă mobilă vor conduce la rezultatul scontat
atunci când veți rotunji afișarea rezultatelor finale la numărul de
zecimale dorit. Chiar dacă funcția "str()" este, de obicei,
suficientă, pentru un control mai fin al printării consultați
specificatorii de format ai metodei "str.format()" din Sintaxa
șirurilor de format.

Pentru cazurile de întrebuințare care au nevoie de reprezentări
zecimale exacte, vă recomandăm să folosiți modulul "decimal" în care
este implementată o aritmetică zecimală potrivită atât pentru
aplicațiile de contabilitate cât și pentru cele de înaltă precizie
(numerică).

Altă formă de aritmetică exactă este oferită de modulul "fractions"
care implementează o aritmetică bazată pe numere raționale (astfel
încât numerele de forma 1/3 să fie reprezentate exact).

În cazul în care sunteți un utilizator asiduu al operațiilor în
virgulă mobilă atunci n-ar strica să aruncați o privire asupra
pachetului NumPy ori asupra oricăruia din numeroasele pachete de
calcul matematic și statistică oferite de către proiectul SciPy. Vezi
<https://scipy.org>.

Python provides tools that may help on those rare occasions when you
really *do* want to know the exact value of a float.  The
"float.as_integer_ratio()" method expresses the value of a float as a
fraction:

   >>> x = 3.14159
   >>> x.as_integer_ratio()
   (3537115888337719, 1125899906842624)

Since the ratio is exact, it can be used to losslessly recreate the
original value:

   >>> x == 3537115888337719 / 1125899906842624
   True

The "float.hex()" method expresses a float in hexadecimal (base 16),
again giving the exact value stored by your computer:

   >>> x.hex()
   '0x1.921f9f01b866ep+1'

This precise hexadecimal representation can be used to reconstruct the
float value exactly:

   >>> x == float.fromhex('0x1.921f9f01b866ep+1')
   True

Fiind o reprezentare exactă, ea este utilă la *portarea* (transportul,
de la englezescul *porting*) în siguranță a valorilor între diferitele
versiuni de Python (oferind, așadar, independența de platforma de
calcul) precum și la schimbul de valori dintre Python și alte limbaje
de programare care suportă acest format (cum sunt Java și C99).

Another helpful tool is the "math.fsum()" function which helps
mitigate loss-of-precision during summation.  It tracks "lost digits"
as values are added onto a running total.  That can make a difference
in overall accuracy so that the errors do not accumulate to the point
where they affect the final total:

>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True


15.1. Erori de reprezentare
===========================

Secțiunea de față se ocupă de exemplul "0.1" în detaliu și vă prezintă
ce aveți de făcut pentru a realiza o asemenea analiză de caz riguroasă
chiar dumneavoastră. Presupunem că dispuneți de o minimă familiaritate
cu reprezentările binare în virgulă mobilă ale numerelor.

*Eroarea de reprezentare* se referă la faptul că anumite (de fapt,
majoritatea lor) fracții zecimale nu pot fi reprezentate exact ca
fracții binare (în baza 2). Această situație este motivul fundamental
pentru care Python-ul (ori Perl-ul, C-ul, C++-ul, Java-ul, Fortran-ul
și diverse alte limbaje de programare) nu afișează, în multe cazuri,
exact numărul zecimal la care v-ați fi așteptat.

De ce asta? Pentru că 1/10 nu este reprezentabil exact ca fracție
binară. Încă din anul 2000, cel puțin, aproape toate mașinile de
calcul folosesc aritmetica binară în virgulă mobilă a standardului
IEEE 754 iar majoritatea platformelor de calcul mapează float-urile
din Python pe valorile binare, date în dubla precizie pe 64 de biți
(*binary64*) a standardului IEEE 754. Valorile binary64 ale lui IEEE
754 conțin 53 de biți de precizie, astfel că, atunci când îl primește
pe 0.1 ca dată de intrare, calculatorul va încerca să-l convertească
în cea mai apropiată ca valoare fracție de forma *J*/2***N*, unde *J*
este un număr întreg de exact 53 de biți. Rescriind relația

   1 / 10 ~= J / (2**N)

drept

   J ~= 2**N / 10

and recalling that *J* has exactly 53 bits (is ">= 2**52" but "<
2**53"), the best value for *N* is 56:

   >>> 2**52 <=  2**56 // 10  < 2**53
   True

That is, 56 is the only value for *N* that leaves *J* with exactly 53
bits.  The best possible value for *J* is then that quotient rounded:

   >>> q, r = divmod(2**56, 10)
   >>> r
   6

Since the remainder is more than half of 10, the best approximation is
obtained by rounding up:

   >>> q+1
   7205759403792794

Așadar, cea mai bună aproximație a lui 1/10 în dubla precizie oferită
de standardul IEEE 754 este:

   7205759403792794 / 2 ** 56

Împărțind atât numărătorul cât și numitorul la 2 (adică, simplificând
fracția cu 2), fracția devine:

   3602879701896397 / 2 ** 55

Remarcați faptul că, deoarece am realizat o rotunjire prin adaos,
această fracție este puțin mai mare decât 1/10; dacă nu am fi folosit
adaosul, atunci câtul ar fi fost puțin mai mic decât 1/10. Însă, în
niciun caz, nu am fi ajuns *exact* la 1/10!

În concluzie, calculatorul nu-l "vede" pe 1/10 niciodată : tot ce
poate vedea este exact fracția dată mai sus, ea fiind cea mai bună
aproximație în dublă precizie pe care o poate construi urmând
standardul IEEE 754:

>>> 0.1 * 2 ** 55
3602879701896397.0

If we multiply that fraction by 10**55, we can see the value out to 55
decimal digits:

   >>> 3602879701896397 * 10 ** 55 // 2 ** 55
   1000000000000000055511151231257827021181583404541015625

meaning that the exact number stored in the computer is equal to the
decimal value
0.1000000000000000055511151231257827021181583404541015625. Instead of
displaying the full decimal value, many languages (including older
versions of Python), round the result to 17 significant digits:

   >>> format(0.1, '.17f')
   '0.10000000000000001'

The "fractions" and "decimal" modules make these calculations easy:

   >>> from decimal import Decimal
   >>> from fractions import Fraction

   >>> Fraction.from_float(0.1)
   Fraction(3602879701896397, 36028797018963968)

   >>> (0.1).as_integer_ratio()
   (3602879701896397, 36028797018963968)

   >>> Decimal.from_float(0.1)
   Decimal('0.1000000000000000055511151231257827021181583404541015625')

   >>> format(Decimal.from_float(0.1), '.17')
   '0.10000000000000001'
