Real.README - documentation for real.py. Goes with version 1.11.
Written by Jurjen N.E. Bos (jurjen@q2c.nl)
Introduction
============
real.py is a library that introduces a new class, called Real, of
abitrarily precise numbers, allowing computations with "infinite"
precision.
Each number keeps an approximation of the accuracy of the number.
Routines are available to compute numbers with more and more digits.
real.py allows you to:
- compute with very high precision, e.g. 1000 digits
- compute with very large or small numbers, up to about 10**500000000
- compute without unexpected precision loss: all digits guaranteed
- compute with infinite precision, printing digits on the fly
- printing floating point numbers to their exact accuracy
Just to whet your appetite:
>>> from real import pa,exp,pi,sqrt
>>> pa('exp(pi()*sqrt(163))')
262537412640768743.99999999999925007259719818568887935385633733699086270
753741037821064791011860731295118134618606450419308388794975386404490572
871447719681485232243203911647829148864228272013117831706501045222687801
444841770346969463355707681723887681000923706539519386506362757657888558
223948114276912100830886651107284710623465811298183012459132836100064982
...
and so on.
The function r() in the library constructs Real numbers, and has a
cornucopia of options. Details are shown below.
The library supports the following operators:
+, -, abs
+, -, *, /, **; also both ways with int
<< and >> with int
int(), long()
The library supports the following mathematical functions:
floor, ceil
sqrt, root (n-th root)
exp, log, log10
atan, asin, acos, sin, cos, tan
sinh, cosh, tanh, asinh, acosh, atanh
fact (for all arguments!)
pi and e are functions taking number of digits (with default)
All of these functions are also available as methods. NumPy users
should be happy with this.
The library supports the following constants, implemented as functions
taking a number of digits:
pi
e
Simple examples
---------------
This library allows calculations with arbirary precision numbers:
>>> from real import *
>>> 1/(pi()**3-31)
159.3198876209530096514514069+-3
pi() is a function computing a given number of digits of pi. The default
is real.default (initially 32):
>>> pi(50)
3.141592653589793238462643383279502884197169399375105+-2
Note the precision indication "+-2" at the end: it means that the last
digit may be 2 too high or 2 too low. In fact, the digit is correct:
>>> pi(52)
3.14159265358979323846264338327950288419716939937510582+-2
The precision indicator is "built in" to every number of the Real
class.
In this way, the digits you get are always right (but not neccesarily
plentiful).
For example:
>>> sin(3L**61)
.940+-1
This should be read as "a number between 0.939 and 0.941".
The class Real allows for very large and small numbers:
>>> fact(10000000)
1.2024234005159034561401534879443075698+-1e65657059
>>> exp(exp(exp(3)))
2.05098643605164889510586094208878506609+-5e229520860
The function r() allows to construct numbers with given precision:
>>> r(3L**61,10)
1.27173474827+-5e29
>>> r(3L**61,50)
127173474825648610542883299603.0000000000000000000000+-5
The function can also be used as a "pretty printer" for floating-point
numbers:
>>> import math
>>> math.pi
3.14159265359
>>> r(math.pi)
3.1415926535897931+-5
The precision of the number math.pi is now explicitly shown.
Infinite precision
------------------
The function pa() prints "all" digits of a number. One of the simplest
examples is computing "all" digits of pi:
>>> pa(pi)
3.1415926535897932384626433832795028841971693993751058209749445923078164
062862089986280348253421170679821480865132823066470938446095505822317253
(and so on).
The routine pa() computes a number repeatedly with increasing precision,
each time printing the digits it can guarantee.
If you pass a string to pa(), you can compute arbitrary expressions:
>>> pa('sqrt(pi()**7-e()**8-39)')
.57899977121221107665115128180224906923190232347298078138819681954282919
919476693269700788120335999771483821199512086460383486796180647225032362
...
About precision
---------------
All computations and numbers in the Real class have a very carefully
defined notion of precision. Normally, when writing "precision" I mean
the number of digits in the mantissa of the number. Thus:
>>> r(3)**123456789
1.17852993898070743873540+-4e58903858
has a precision of just under 24 digits. Internally, the precision is
stored in bits, but you don't have to know that.
The default precision is real.default, which is initially set to a
generous 32 digits:
>>> pi()
3.141592653589793238462643383279503+-2
>>> real.default=20
>>> pi()
3.141592653589793238462+-2
The number 0 does not have a precision, so when a precision is needed,
it is taken to be the default precision.
A computation may suffer from precision loss. For example,
the above mentioned computation:
>>> real.default=32
>>> sin(3L**61)
.940+-1
The reason this approximation is so inaccurate is that the last digit
of the default approximation of the number 3**61 influences the result
by 0.001. Since the sine function's result cannot be more accurate than
its argument, the result can also not be more accurate than 0.001. So,
the sin function cannot guarantee more digits, if no more digits of
3**61 are known. We can use r() to get more digits:
>>> sin(r(3L**61,50))
.940023765661479919109+-1
Now the result is much more accurate, but still less than the 50 digits
of the argument, of course.
Let's compare this with the built-in floating point numbers:
>>> import math
>>> math.sin(3L**61)
-0.700599464638
What? All digits (and even the sign) are wrong! This is not because
there is a bug in the math library; it is because the floating-point
representation of 3**61 is not accurate to the last digit:
>>> float(3L**61)
1.27173474826e+29
This implies that the sin gives a value based on a number that is
useless. This also shows one of the powers of "real": it does not
give wrong answers.
If a function has very smooth behaviour, it is also possible that it
returns more digits than its argument:
>>> r(10)**10
10000000000.00000000000000000000+-3
>>> atan(r(10)**10)
1.5707963266948966192313216916400847754320+-2
Now we have no less than 41 digits of precision. As a rule, all
functions produce at most twice the precision of their input by rounding
if necessary; this is to prevent excessive computation times.
Mixed arithmetic
----------------
Number in the Real class may be mixed with integers in computations.
Integers and longs are assumed to be _exact_ by the +,_,*,/ operators:
>>> pi()
3.141592653589793238462643383279503+-2
>>> pi()-3
.141592653589793238462643383279503+-2
You can see that no precision loss occurs in the subtraction.
The integer powers -1, 0, 1, 2, 3 are implemented as multiplication or
divisions for speed and accuracy:
>>> 1/pi()
.318309886183790671537767526745029+-1
>>> pi()**-1
.318309886183790671537767526745029+-1
>>> pi()**r(-1)
.3183098861837906715377675267450+-1
Float and strings are converted using the function r() if they are
mixed with Reals: see below.
>>> pi()+math.pi+"3.14159"
9.42474+-4
>>> sqrt(1.5)
1.2247448713915889+-3
The function r(): constructing Reals
====================================
The function r() allows to construct Real numbers from integers, longs,
fractions, floating point numbers and strings.
Constructing Reals from integers and longs
------------------------------------------
r(n) makes n into a real number, with default precision.
r(n,p) makes n into a real number, with precision of p digits. There is
no loss of accuracy in the conversion:
>>> r(1)
1.00000000000000000000000000000000+-1
>>> r(1,10)
1.0000000000+-1
Construction of Reals from fractions
------------------------------------
r(n,d,p) makes n/d into a real number, with precision of p digits. There
is (essentially) no loss of precision in the conversion:
>>> r(1,3,20)
.333333333333333333332+-4
To get the default precision, use real.default or None for p.
Construction of Reals from floating point numbers
-------------------------------------------------
r(f) makes a floating point number into a real with the (at for my
machine) correct amount of digits: 53 bits. This indicates nicely
how accurate the number is:
>>> math.pi
3.14159265359
>>> r(math.pi)
3.1415926535897931+-5
This shows that the default printing routine of floating point numbers
hides some of the significant digits.
It is also allowed to use r(f,p) to get p digits of a floating point
number, but extra digits are "invented" if you increase the precision
beyond that of the floating point number. Be warned!
>>> r(math.pi,30)
3.141592653589793115997963468544+-4
>>> pi(30)
3.1415926535897932384626433832795+-2
Construction of Reals from Reals
--------------------------------
r(x), where x is a real, just returns x.
r(x,p) changes the precision of x to p. The same warning as above
applies: don't increase the precision unless you don't care about the
actual value, or you know what you are doing.
>>> r(pi(30),10)
3.1415926537+-3
>>> r(pi(10),30)
3.141592653584666550159454345703+-4
Construction of Reals from strings
----------------------------------
To allow you to convert the representation format back to a number, you
can construct a Real from a string. The format is:
number = mantissa [error] [exponent]
mantissa = ["+"|"-"] digit* ["." digit*]
error = "+-" digit
exponent = ("E"|"e") ["+"|"-"] digit+
If the period is omitted, it is assumed to be at the end of the
mantissa. If the mantissa is omitted altogether, it is assumed to be
"0".
If the error digits are omitted, it is taken to be +- one half of the
last digit.
If the exponent is omitted, it is taken to be e0.
The string representation of a Real is also in this format. However, due
to roundoff, the error estimation may increase a bit if a number is
converted to string and back.
Also here, r(s,p) may be used to modify the precision of the result
(with the same caveat as above: don't increase the precision if you
want correct accuracy!)
>>> r('1234.567e3')
1234567.0+-5
>>> r('1234.567+-1e3')
1234567.+-1
>>> r('1234.5678e3')
1234567.8+-1
>>> r('1234.5678e3',20)
1234567.81250000000000+-2
Precision of 0
--------------
As a special rule, if you let r() compute 0 to a given precision, it
will take the second argument as "digits after the point" instead of
"mantissa digits":
>>> r(0,10)
.00000000000+-3
Implicit conversions
--------------------
r() is also called implicitly if a function from real.py is called with
an argument that is not a Real:
>>> fact(10)
3628800.00000000000000000+-1
>>> sin(math.pi)
.000000000000000+-1
>>> cos('0.000010000000')
.999999999951+-2
Using the library
=================
The most useful (and fun) functions of the library are rep() and pa().
The function rep()
------------------
The function rep computes the result of an expression using increasing
precision. rep() accepts a function from number of digits to result, but
also a string which is interpreted as an expression:
>>> rep(e)
2.718281828459045235360287471352663+-2
2.71828182845904523536028747135266249775724709+-2
2.71828182845904523536028747135266249775724709369995957496696+-1
...
>>> rep('sin(1)')
.84147098480789650665250232163030+-2
.8414709848078965066525023216302989996225630+-2
.8414709848078965066525023216302989996225630607983710656728+-2
...
rep() is designed so that the amount of time to compute any result is
not more than twice the time spent since the start of the function. This
means that if you want as much precision as you can get, you don't have
to wait more than twice as long as you would wait computing to only that
precision.
This is very useful if you don't want what precision to choose.
The optional second argument of rep() gives the precision of the first
computation:
>>> rep(pi,3)
3.1416+-2
3.14159+-2
3.1415926+-2
3.141592653+-2
3.141592653590+-2
3.1415926535897932+-2
3.141592653589793238462+-2
3.1415926535897932384626433833+-2
3.1415926535897932384626433832795028842+-2
3.1415926535897932384626433832795028841971693993752+-2
...
The function pa()
-----------------
pa() accepts the same arguments as rep(), but attempts to print the
result only once, adding new digits as soon as they are computed:
>>> pa('sin(1)')
.84147098480789650665250232163029899962256306079837106567275170999191040
439123966894863974354305269585434903790792067429325911892099189888119341
032772921240948079195582676660699990776401197840878273256634748480287029
...
The function pa() cannot print numbers that end with zeroes, since it
can never be sure whether this is not a really long sequence of nines.
In particular, something as simple as pa('1+1') does _not_ give an
answer! For these cases, rep() can be used as a replacement.
>>> pa('r(123456789)')
12345678
To show a nice example why pa() cannot print the next digit, try:
>>> pa('log10(sin(10**-r(5432,100,None)))',8)
-54.32000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000001658185300612831004479789196460
369758485244604552424332996973775238492796390137138858936927630712568532
...
>>> pa('log10(tan(10**-r(5432,100,None)))',8)
-54.31999999999999999999999999999999999999999999999999999999999999999999
999999999999999999999999999999999999999996683629398774337991040421607079
260483029510790895151334006052449523014407219725722282126144738574862934
...
In both cases, after having printed "-54.3", pa() takes some time
computing enough digits to decide if the next digit is a 1 or a 2. The
difference is _very_ small, hence the delay.
Also note the use of r(5432,100,None) to get the constant 54.32 to
default precision.
The factorial
-------------
This will probably not surprise you:
>>> fact(10)
3628800.00000000000000000+-1
In fact, the factorial function does a lot more than multiplying
numbers. The fact() function actually compute the gamma function of
n+1:
>>> fact(10+r(1,10,None))
4593083.5895600181037600919+-2
We can check if the gamma function of 1/2 is indeed sqrt(pi), as is
claimed in the schoolbooks:
>>> fact(r(1,2,50)-1)
1.77245385090551602729816748334114518279754946+-1
>>> sqrt(pi(50))
1.772453850905516027298167483341145182797549456122387+-2
The function can also produce very small numbers:
>>> fact("-10000000.500000000000000")
8.2621381+-5e-65657056
Actually, the function fact() is the most complicated funcion of the
entire library. (And that's just why I wrote it :-)
Precision
---------
Sometimes, strange things happen with precision. Don't let yourself
be fooled. An example:
>>> exp(pi(31)*sqrt(163))
262537412640768744.000000000000+-2
The most plausible conclusion is that exp(pi*sqrt(163)) is a whole
number, but we all know that's pretty unlikely. In fact, this is just
a freak event, as you can see by increasing the precision:
>>> real.default=50
>>> exp(pi()*sqrt(163))
262537412640768743.999999999999250072597198185690+-2
Differential quotients
----------------------
The differential quotient of a function is defined as:
(f(x+h)-f(x))/h
Since math class we know that if h goes to zero, the result of the
quotient goes to the derivative of f in point x: f'(x).
This differential quotient can be used to compute derivatives, if h is
chosen carefully. For numerical reasons too hard to explain here, the
correct value to choose for h is 10**(-default/2). This works for all
functions that have reasonable behaviour (i.e. the second derivative is
smaller than, say, 100).
This trick can be used to print one of the most complicated mathematical
constants, called Euler's constant (denoted by lower case gamma).
We use that gamma = -fact'(0), and fact(0)=1.
(By the way: don't expect 10000 digits from this computation, unless you
have an exceptional computer. This computation is _slow_. I never came
any further than 1331 digits.)
>>> pa('(lambda d:(1-fact(d))/d)(10**-r(real.default/2))')
.57721566490153286060651209008240243104215933593992359880576723488486772
677766467093694706329174674951463144724980708248096050401448654283622417
399764492353625350033374293733773767394279259525824709491600873520394816
567085323315177661152862119950150798479374508570574002992135478614669402
960432542151905877553526733139925401296742051375413954911168510280798423
...
FAQ
===
- I compute pa('1+1'), and nothing happens. Why?
* Computing 1+1 gives 2.0000000000000000..., as we all know. Computing
this number to any precision cannot say whether it should be printed
as 1.9999999999999999 or 2.0000000000000000, since the next digit
may influence the result. Therefore, pa() cannot even print the first
digit.
- Why does fact(r(1,1000)) take forever?
* It doesn't take forever, but it takes a very long time. This is
because computing factorials in general is very complicated. As a rule
of thumb, computing factorials small factorials (<10000) to large
precisions (more than 1000) digits takes very long.
- How can ceil(x) be defined as floor(x)+1?
* No, I'm not giving it away that easy. Use the hint in the source
code. If you still don't see it, mail me and I'll tell you.
- What the heck is a Bernoulli number, and what do they have to do
with factorials?
* Read for example Knuth, volume 1, chapter 1.2.9.
- What does hex() do for Real numbers?
* It gives a representation of the number in "floating point binary",
represented with a hex mantissa and exponent. There are people who
actually use this :-)
- Why are there methods for the mathematical functions?
* It allows you to say x.sin() instead of sin(x). It is useful for
people who use the NumPy extension; see
Implementation comments
=======================
I started this project to learn about Python programming. I decided to
do something quite challenging, so I could see if learning the language
distracted me from the real problem. It didn't, and I was quite
impressed.
This ease of use allowed me to write something much more advances than I
expected. I would never have guessed that I would ever write a factorial
routine with arbitrary precision!
To me, this project showed that Python is indeed capable of serious
programming. One thing I did not do, was user interfaces: everything
here is completely machine independent. Therefore, I cannot comment on
the portability aspect.
Of course, I like the syntax of Python, but I especially love its
semantics:
- The cooperation between the operators >>, /, % and & is amazing.
I do not know of any other language that does this:
x>>2 = x/4 for all x
x&3 = x%4 for all x
x&-2 = x-x%2 for all x
- The "and" and "or" operators are very nice, and much more useful than
their C counterparts. (But this is not new, of course.)
- The name semantics are so clean that I start to hate C.
- The exceptions.
- Debugger and profiler written in Python.
These things combined made me a total Python convert. You have to force
me to use anything else for programming :-)
BTW, if you like to be amazed, use the profiler to see what happens if
you compute fact(r(1,200)). It gives you an idea of the complexity of
the underlying mathematics.
There were also a few things that I didn't like about python, especially
about its implementation:
- the fact that you get an "outrageous left shift count" error on big
left shifts, for example 1L<<100000. Why is this? I know the result is
big, but I really _want_ that. To be precise, there are no less than
36 places in the program where I want to be able to do that. I had to
write a function that does it, and use that instead. At one point, I
even had to inline the function for speed reasons.
- the difficulties computing the number of significant bits of a long
or int. Check out my function bitsize. It is quite dirty, although it
will probably never fail, and it will certainly not give wrong
answers. It took me a long time to write it. There are also four
places in the program I had to do a weird trick not to have to compute
it for speed reasons.
- the time cost of a function call. I hope someone will invent a
modification of the implementation of function call that does not
change the beautiful semantics.
- O yes, and the long number routines could be faster. But I shouldn't
complain :-)
Conclusion
==========
Thanks to Tim Peters and Christian Tismer for their interesting and
useful discussions, and their help and advice with making this library
into a mature product.
I hope you enjoy the library. If you like it, send me a postcard:
Jurjen N.E. Bos
Deukelven 48
1963 SR Heemskerk
The Netherlands
I'd like to know what you use it for.
- Jurjen N.E. Bos (jurjen@q2c.nl)