Newbie question about 'decimal' calculations

They are at least more understandable in terms of exactness and rounding errors.

The problem here is quite simple and is just that programming languages such as Python use the obvious decimal literal notation for creating binary floats even though most decimal literals cannot be represented as binary floats. By design we have built an entirely avoidable rounding error into the first step of any naively written non-integer calculation.

Changing that only helps in very simple cases though because other things will still have rounding errors and e.g. your x_{n+1} = 2 x_n - 0.3 recurrence is inherently unstable: nothing short of fully exact arithmetic could prevent all runaway for something like this.

2 Likes

But. e.g., 1 / 49 * 49 does not. Such failures have much more to do with “finite precision floating point” than with the base used.

What most newbies really want, even if they lack the vocabulary to say so, is unbounded precision rationals. Which is fractions.Fraction() in Python. I’ll skip references but multiple projects have concluded that. The computer implementation of unbounded rationals sacrifices none of the field properties of mathematical rationals. No representation errors, no rounding errors, + and * are associative and commutative, when they’re defined additive and multiplicative inverses exist and are exact, and so on.

Python’s interface to them could be improved for newbies, though. The most heavily used “calculator program” in the world is probably the one that ships with Windows. While the docs say nothing about this, and nothing about the internals are visible from the UI, it uses unbounded rationals under the covers.

Which users don’t see. What they see is what appear to be decimal floats. Divide, e.g., 100 by 3, and what you see is 33.33333333… However, multiply that by 3, and you get back 100. It’s not WYSIWYG, but “what you typed in is what you get - although you may not see all of it”.

As a general-purpose “solution”, though, they aren’t. Python’s closest predecessor language was ABC, a research language designed for newbies at CWI (a Dutch research institute). Guido worked on its implementation, which is why Python didn’t have unbounded rationals built in :wink:. They have many lovely properties, but in real life routinely consume all RAM and slow down horribly as computations go on.

That’s largely invisible in the Windows context, because it’s not programmable. People type in one computation at a time, by hand. If an operation is a million times slower than it would have run in HW floats, and consumes 100 MB of RAM, they probably won’t notice.

ABC eventually, and reluctantly, added machine floats too. Python eventually, and reluctantly, added fractions.Fraction.

Still, when I don’t want to bother with numerical analysis at all, and don’t have irrational numbers to deal with. fractions.Fraction is often my first choice. It’s very satisfying when, e.g., you’re building a large table of probabilities from recurrence relations, and, at the end, can assert that the final values add to exactly 1.

Don’t overlook that binary floats are implemented directly in hardware now. That’s overwhelmingly the primary reason they run so much faster. One of Cowlishaw’s motivations for pouring so much effort into standardizing decimal floats was to nudge IBM into putting more effort into supporting them in HW too. I don’t think he’s had much success at that, though.

Ya, but I’ve been in this community for decades. The kind of people who accept “challenges” didn’t show up in this topic :wink:. So I used it as an opportunity to flesh out a related topic a bit.

BTW, using fractions.Fraction(), there is no challenge to speak of.

>> from fractions import Fraction as F
>>> p1 = F(1, 10)
>>> p2 = F(2, 10)
>>> p3 = F(3, 10)
>>> p1, p2, p3
(Fraction(1, 10), Fraction(1, 5), Fraction(3, 10))
>>> a = p1 + p2
>>> a == p3
True
>>> while True:
...     a = 2 * a - p3
...     print(a)
...
3/10
3/10
3/10
3/10
,,, and so on, until the HW burns out ...
1 Like

:slight_smile: … it becomes better and better … :slight_smile:

I’d rather call is sensible than unstable, and expect silly iterations stable while the basic steps are exact, what I expect fulfilled while not overstretching digits / bits available, and see that ok for addition, subtraction, multiplication and! inverse multiplication of reasonably short decimals, correct?

@ Tim: calculator program in in windows …

are you talking about ‘calc’ or Excel? If Excel I’d like to offer a bet that you are wrong.

But. e.g., 1 / 49 * 49 does not.

I said ‘better’, not ‘good’.

is unbounded precision rationals

near, but not hit, very few of them need more than 1E-5 … 1E+13 range or more than 10 digits precision, what they’d like, inherently demand, but lack wording about is 'qualified math", or ‘math logic’!

And again, wonderful crash course in ‘fractions’, :slight_smile: yes, fractions have better math power than decimals, they are a superset of them as decimals are to binaries. The only point silly people like me often demand decimal is that we got pissed about fraction math in school, didn’t like it and got trained to decimals with their shortcomings.

If an operation is a million times slower than it would have run in HW floats, and consumes 100 MB of RAM, they probably won’t notice.

but datacenters would and likely melt down, that’s why I’m investigating what’s under the hood, and e.g. seeing ‘tangent’ factor ~30 000 slow in IEEE dec128 vs. bin64 shows there is a problem, and not faster in dec64 or dec32 shows that improvements should be possible.

Think there are howtos available for e.g. interval and fractions, but newbies don’t find them in time, they even don’t know why and what to seek for. See that for floats improved recently, besides it has already been very good, doe’s it have hints that interval arithmetic doesn’t, but fractions can solve most ‘silly problems’?

Tim, did you write a book ‘introduction to math in computers’? If not it might be a fantastic idea to do so.

Thank you so much for such a detailed, thoughtful, and inclusive response, Oscar! I believe I have a far better understanding now of what you’re aiming at.

And it makes sense to me - I have a lot of sympathy for all your goals. Then again, I also have sympathy for a newbie who just goes “WTF can’t I ask for the sine of a Decimal?” :wink:.

This probably won’t help, but … if you find a package that meets your goals except for the license, it can work to ask the author if they’re willing to let you _re_license it for inclusion in your derived work.

That’s basically “the norm” for contributions to CPython. See here. Contributors can distribute their work to anyone they like, under any license(s) they like, but they have to license it to the PSF under (their choice) the Academic Free License v 3 or the Apache License v 2, and allow the PSF in turn to release it under that initial license, or any other open source license approved by the PSF Board.

I personally wish that License Hell would just go away :frowning_face:.

Neither. At least since Windows10, CalculatorApp.exe.

Excel very much embodies “the fast drives out the good”. The modern calculator app is focused far more on delivering results “naive” people expect, regardless of cost (although stopping far short of trying to implement “the constructive reals”).

Very debatable, and I’m not going there :wink: Microsoft had years & years & years of feedback about their string of calculator apps, and I doubt anything about the current product is an accident.

A curiosity (to me) is that it’s limited to values of magnitude < 1e10000. Why an exponent of 10 thousand? Don’t know. But I’m sure it’s quite deliberate that if a computation result exceeds that limit, it screams “Overflow” at you instead of returning an infinity.

Yes, they expect numbers to obey the rules of a mathematical field, even if they can’t name them. Unbounded rationals do. Floats (regardless of base) don’t.

Long ago I wrote the Python tutorial Appendix on floating-point issues. As Python grew, more people added more words about more features, but I think it retains its original “welcome! don’t panic!” :wink: nature.

But, at the start, things like decimal and fractions and numpy didn’t exist. The number of number-ish packages available to Python (both builtin and external) programmers now would a require a small book of its own just to give a very high-level overview.

‘floating-point issues’: what about adding 'the binary floating-point math commonly used in computers has some shortcomings once it comes to decimal fractions. If you need math that better matches what you learned in school, e.g. 0.2 + 0.1 resulting in 0.3 rather than 0.30000000000000004, the decimal module → [link to basic howto] might be an option. Note that coding is a little more complex, execution somewhat slower and not all functions available. If you need to go beyond that and want qualified fraction math, where x / y * y comes back to x, you might want to try the fraction module → [link to basic howto], note that also restrictions apply.

Within the first three paragraphs! Or similar.

Well there has to be some limit and very few users have any need to go larger than that. This is part of what I meant when saying that mpmath has unbounded computational cost:

In [1]: import flint

In [2]: ten = flint.arb(10)

In [3]: ten ** ten
Out[3]: 10000000000.0000

In [4]: ten ** ten ** ten
Out[4]: [1.00000000000000e+10000000000 +/- 3.2e+9999999982]

In [5]: ten ** ten ** ten ** ten
Out[5]: [+/- inf]

In [6]: import mpmath

In [7]: ten = mpmath.mpf(10)

In [8]: ten ** ten
Out[8]: mpf('10000000000.0')

In [9]: ten ** ten ** ten
Out[9]: mpf('9.9999999999999998e+9999999999')

In [10]: ten ** ten ** ten ** ten
^C^C^C^C^\Quit (core dumped)

Ctrl-C didn’t work for me there but trusty Ctrl-\ saves the day.

Limiting the exponent needs to be built in from the ground up. The simplest way is just to set a hard global limit. Then all of the algorithms for sin, exp etc can be implemented with those limits in mind.

In Arb the exponent range is connected to the precision so approximately if you work at precision p then you can have about 2*p bits in the exponent. This approach satisfies the properties I laid out before that you have bounded computation but if p is made large enough then any well defined result will eventually be computed accurately. Of course no floating point approach will succeed in reasonable time and space if I keep adding ** ten so there always needs to be some finite limit.

The decimal module has Emax and Emin which you could use for this but there are upper and lower limits for those:

In [27]: decimal.MAX_EMAX
Out[27]: 999999999999999999

In [28]: decimal.MIN_EMIN
Out[28]: -999999999999999999

That gives about 60 bits for the exponent which I suppose is represented as a 64 bit signed integer. It is unfortunate that we can’t go further but it is also important that there is always some bound on the exponent.

With mpmath anything can be computed if you have enough RAM etc but there is no bound on the computational cost of calling any given function. With decimal the computational cost is always bounded but there is also a ceiling on what can be computed. That exponent ceiling is far lower than the practical limits of what can be done in real computers even if it is also far higher than most people would need.

With Arb everything is bounded but you always have the option to increase the bounds as far as your hardware will allow. Once you have made the decision that the exponent will be an arbitrary precision integer there is no need to limit its size any more so than the size of the mantissa.

As an aside since this is not obvious from the output Arb has three kinds of infinity although they all print the same:

In [1]: import flint

In [2]: pinf = flint.arb('inf')

In [3]: ninf = flint.arb('-inf')

In [4]: pminf = flint.arb(1e100).exp()

In [5]: [pinf, ninf, pminf]
Out[5]: [[+/- inf], [+/- inf], [+/- inf]]

In [6]: [float(pinf), float(ninf), float(pminf)]
Out[6]: [inf, -inf, 0.0]

In [7]: [pinf.rad(), ninf.rad(), pminf.rad()]
Out[7]: [0, 0, [+/- inf]]

In [10]: [pinf*0, ninf*0, pminf*0]
Out[10]: [nan, nan, [+/- inf]]

The overflow from exceeding the exponent range returns pminf rather than the other two infinities that are only created if you create them directly. I think pinf and ninf are only there so that any float can be represented as an arb.

The pminf result is an infinitely wide interval rather than an infinite value. Any calculation that can actually have infinite values results in nan:

In [11]: (flint.arb.pi()/2).tan()
Out[11]: nan
1 Like

inf is a float, as ordinary and as special as 1, 0, or 2.22044604925e-16.

Computing with floats, with their arithmetic operations, you never obtain something that is not representable as a float.

You just didn’t ask the question that you intended.

“Unstable” is a technical word in dynamical systems, and it was used above with its correct meaning.

2 Likes

This is something, that might be solved in the mpmath. I wouldn’t say that put restriction on the exponent range for the mpf type is an easy task, but it’s doable. And this context will be more close to IEEE floating-point arithmetic (for good or bad).

The IV backend (which I would prefer to replace to something closer to arb) will immediately benefit from this change too.

I was thinking about more and rather now skeptical about such approach. Hardware is still binary and such library of functions will have some benefits over binary floating-point arithmetic only due C-level implementation. Say,

$ python -m timeit -s 'from mpmath import log, mp;mp.dps=100' 'log(10)'
10000 loops, best of 5: 27.6 usec per loop
$ python -m timeit -s 'from decimal import getcontext; ctx=getcontext();ctx.prec=100' 'ctx.ln(10)'
50000 loops, best of 5: 5.86 usec per loop
$ python -m timeit -s 'from _pydecimal import getcontext; ctx=getcontext();ctx.prec=100' 'ctx.ln(10)'
1000 loops, best of 5: 213 usec per loop
$ MPMATH_NOGMPY=Y python -m timeit -s 'from mpmath import log, mp;mp.dps=100' 'log(10)'
10000 loops, best of 5: 24.6 usec per loop

Also, I often find myself using SymPy (well, Diofant:)) to generate C code for numerics. Using binary floating-point numbers there and decimal numbers in the Diofant will be a nightmare, IMO.

In principle, I don’t see why it can’t be done without interval arithmetic. You can repeatedly evaluate all polynomials with increasing precision, until some evaluated to nonzero with say 2 digits, then exclude it. Repeat, until there is only one polynomial.

2 Likes

It would be worthwhile to do these things. As you say it is not easy though.

You have to track the bounds through every step of the calculation or otherwise you can’t say that anything is nonzero to 2 digits just by looking at the final output. Tracking these bounds through every step is precisely what ball arithmetic does.

It is far better to keep track of a radius rather than vaguely defined notions of “precision” or “accuracy”. It is also important that each function gets to choose precisely which radius it returns so that the correctness and performance of that function is clearly defined and responsibility for ensuring it lies in one place. If the caller is trying to estimate the radius then all correctness is fragile.

2 Likes

Diofant’s default is evalf(strict=True), I tried to keep minimal number of cases where this is overridden. Though you are right, it’s a kind of heuristic and may fail, theoretically.

If you look at what evalf is doing when it tracks the “accuracy” then you can see that it is just a heuristic implementation of ball arithmetic that mostly works but has no formal guarantees.

Noticeable bugs are rare but not theoretical:

In [1]: from diofant import *

In [2]: e = re(1/((I*(-19 + 3*sqrt(33)))**(Rational(1,3))*(-1 + sqrt(3)*I)))

In [3]: e
Out[3]: 
  ⎛                  1                   ⎞
re⎜──────────────────────────────────────⎟
  ⎜   ____________________               ⎟
  ⎜3 ╱   ⎛          ____⎞  ⎛       ___  ⎞⎟
  ⎝╲╱  ⅈ⋅⎝-19 + 3⋅╲╱ 33 ⎠ ⋅⎝-1 + ╲╱ 3 ⋅ⅈ⎠⎠

In [4]: for n in range(20): print(e.evalf(n))
-0.e-5
-1.e-6
-2.4e-7
-1.49e-8
0
-1.1642e-10
0
0
-1.1368684e-13
-1.42108547e-14
-8.881784197e-16
-1.1102230246e-16
-1.38777878078e-17
-8.673617379884e-19
-2.1684043449710e-19
0
-1.694065894508601e-21
-1.0587911840678754e-22
-1.32348898008484428e-23
-1.654361225106055350e-24

This is computing the real part of a pure imaginary number that results from multiplying two complex numbers with nontrivial real and imaginary parts. The true answer is zero. The techniques used here cannot prove that it is zero though so the cases that just print 0 are actually just as wrong as the ones that give a nonzero result with false precision. The correct answer in diofant should be to raise PrecisionExhausted.

The errors are caused by not keeping track of the uncertainty in the real and imaginary directions correctly. It would not be hard to fix this particular particular example but the fix is basically just changing evalf_add to use complex ball arithmetic properly. Really all of it should be rewritten to use ball arithmetic but it will never work to implement ball arithmetic heuristically over the top of mpmath. The function like mpmath.sin that computes the values needs to be the one that computes the radius of the ball as well: you can’t split that responsibility up.

2 Likes

Just a note to say that arb was new to me, and that your explanations have, I think, greatly helped me “get the point” (or some of the points). Some musings:

  • Mathematically, balls are a “more intuitive” basis than intervals, thanks to ubiquitous epsilon-delta approaches and proofs in real & complex analysis.

  • “Correct rounding” can be built on top of the properties you listed, but isn’t needed to achieve those properties.

  • That most packages now build on intervals instead appears, to me, largely due to that IEEE-754 mandated the directed rounding modes that made narrowest-possible bounds “relatively cheap” to compute for +-*/ applied to pairs of floats. But it’s not really individual operations we care about: it’s the final results of perhaps many millions of operations. It doesn’t matter if the bounds are “sloppy” provided there’s a reasonably efficient way to make them ever tighter. Which may not even be needed if the first try proves to be “tight enough”.

  • While the endpoints of an interval both need to be computed to full precision, only the midpoint of a ball does. It the ball is small enough to be useful, the radius gets ever smaller when increasing precision.

So there’s a lot right there aleady to like. Cool! :smile:

But you understood what he meant - and so did everyone else. Excessive pedantry isn’t really helpful.

For example, your 2.22044604925e-16 isn’t representable as an IEEE binary double, if I read it as a decimal literal. You “intended” to say “the closest IEEE binary double to the decimal value I typed”. Which everyone knows that you meant too. My pointing it out is, I agree, tedious :wink:,

But I in turn “intended” to say: this isn’t an academic journal, so let’s please cut non-experts some slack. They bring curiosity and fresh questions to a conversation, and that’s contribution enough.

1 Like

I’m afraid that @oscarbenjamin is right, that without rigorous error bounds attached you really can’t say that “2 digits” is enough to conclude “not 0”.

There’s a related problem in the “constructive reals”: equality is formally undecidable (not always computable). Building on that, your procedure would work: generate more and more digits. If you ever get a digit that isn’t 0, you know for sure that the true result isn’t 0. But if the true result is 0, the computation will (in general) never end.

This didn’t really dodge the “rigorous error bounds” requirement, though: implementations of the constructive reals typically guarantee that the true result is strictly less than 1 ULP away from the digits already delivered. So the partial results themselves come with a rigorous - and tight - implied error bound.

In high precision calculations computing the radius is essentially free. The realisation then is that if you are doing high-precision floating point then maybe you should just always use ball arithmetic. The midpoint of the balls is the same as you would have gotten anyway so you can just discard the radius at the end if you don’t need it.

I think many people would really like arb but just don’t know about it. We need to expose more of its functionality in python-flint and make the interfaces nicer.

2 Likes

I thought I would just quickly list some features of Arb that are available via python-flint (pip install --pre python-flint). All of these things come with the same correctness bounds that I mentioned above.

Firstly some background:

  • Arb was created by Fredrik Johansson as a library that depended on FLINT where FLINT is about exact things like polynomials with integer coefficients and so on but Arb is about robust approximate numerics.
  • Fredrik also created python-flint which wrapped FLINT and Arb for use in Python but then python-flint sort of fell by the wayside. I took over as maintainer of python-flint and fixed it up so that it has wheels, works on Windows etc.
  • Fredrik then took over as maintainer of FLINT and merged Arb and some other things into FLINT.

So the situation now is that Arb does not exist as an independent library but is part of FLINT and python-flint is a wrapper around FLINT that exposes various things including Arb. Fredrik is maintainer of FLINT and I am maintainer of python-flint.

In general you can configure python-flint by assigning to the global (not thread-safe) context attributes:

In [25]: import flint

In [26]: flint.ctx
Out[26]: 
pretty = True      # pretty-print repr() output
unicode = False    # use unicode characters in output
prec = 53          # real/complex precision (in bits)
dps = 15           # real/complex precision (in digits)
cap = 10           # power series precision
threads = 1        # max number of threads used internally

The main things are prec and dps which set the precision for Arb:

In [19]: flint.ctx.prec = 53

In [20]: flint.arb(2).sqrt()
Out[20]: [1.41421356237309 +/- 5.15e-15]

In [21]: flint.ctx.prec = 200

In [22]: flint.arb(2).sqrt()
Out[22]: [1.4142135623730950488016887242096980785696718753769480731767 +/- 2.17e-59]

You can use Arb to do real or complex calculations involving a large array of functions:

In [50]: a = flint.arb(1)

In [51]: a
Out[51]: 1.00000000000000

In [52]: a.exp()
Out[52]: [2.71828182845905 +/- 5.36e-15]

In [54]: ' '.join(n for n in dir(a) if not n.startswith('_'))
Out[54]: 'abs_lower abs_upper acos acosh agm airy airy_ai airy_ai_zero airy_bi airy_bi_zero asin asinh atan atan2 atanh backlund_s bell_number bernoulli bernoulli_poly bessel_i bessel_j bessel_k bessel_y beta_lower bin bin_uiui bits ceil chebyshev_t chebyshev_u chi ci const_catalan const_e const_euler const_glaisher const_khinchin const_log10 const_log2 const_sqrt_pi contains contains_integer contains_interior cos cos_pi cos_pi_fmpq cosh cot cot_pi coth coulomb coulomb_f coulomb_g csc csch digamma ei erf erfc erfcinv erfi erfinv exp expint expm1 fac fac_ui fib floor fmpq fmpz fresnel_c fresnel_s gamma gamma_fmpq gamma_lower gamma_upper gegenbauer_c gram_point hermite_h hypgeom hypgeom_0f1 hypgeom_1f1 hypgeom_2f1 hypgeom_u imag intersection is_exact is_finite is_integer is_nan is_zero jacobi_p laguerre_l lambertw legendre_p legendre_p_root legendre_q lgamma li log log1p log_base lower man_exp max mid mid_rad_10exp min nan neg neg_inf nonnegative_part overlaps partitions_p pi polylog pos_inf rad real rel_accuracy_bits rel_one_accuracy_bits repr rgamma rising rising2 rising_fmpq_ui root rsqrt sec sech sgn shi si sin sin_cos sin_cos_pi sin_cos_pi_fmpq sin_pi sin_pi_fmpq sinc sinc_pi sinh sinh_cosh sqrt str tan tan_pi tanh union unique_fmpz upper zeta zeta_nzeros'

I think there are more Arb functions in FLINT that are not yet exposed in python-flint. These functions are all available as methods on the arb which is an odd interface. It would be better to have contexts with methods like ctx.sin and functions like sin that use a global context. This needs a bit of careful design though because there are many more things in FLINT than just Arb and we probably want a general approach to contexts/domains rather than just something that is specific to Arb.

For complex calculations use acb rather than arb:

In [55]: flint.acb(-1).sqrt()
Out[55]: 1.00000000000000j

There are real and complex Arb matrices:

In [30]: M = flint.arb_mat([[1, 2], [3, 4]])

In [31]: M
Out[31]: 
[1.00000000000000, 2.00000000000000]
[3.00000000000000, 4.00000000000000]

In [32]: M.eig()
Out[32]: 
[[-0.37228132326901 +/- 7.54e-15] + [+/- 3.02e-15]j,
 [5.3722813232690 +/- 2.07e-14] + [+/- 3.02e-15]j]

In [33]: M.det()
Out[33]: -2.00000000000000

In [34]: M.inv()
Out[34]: 
[[-2.00000000000000 +/- 1.63e-15],   [1.00000000000000 +/- 7.41e-16]]
[ [1.50000000000000 +/- 9.44e-16], [-0.500000000000000 +/- 4.17e-16]]

You can use Arb to compute series involving many functions:

In [35]: x = flint.arb_series([0, 1])

In [36]: x
Out[36]: 1.00000000000000*x + O(x^10)

In [37]: x.exp()
Out[37]: 1.00000000000000 + 1.00000000000000*x + 0.500000000000000*x^2 + ([0.166666666666667 +/- 3.71e-16])*x^3 + ([0.0416666666666667 +/- 4.26e-17])*x^4 + ([0.00833333333333333 +/- 4.61e-18])*x^5 + ([0.00138888888888889 +/- 2.23e-18])*x^6 + ([0.000198412698412698 +/- 4.64e-19])*x^7 + ([2.48015873015873e-5 +/- 1.84e-20])*x^8 + ([2.75573192239859e-6 +/- 3.91e-21])*x^9 + O(x^10)

You can compute the roots of any polynomial:

In [45]: x = flint.arb_poly([0, 1])

In [46]: p = x**3 + 2*x**2 + 1

In [47]: p
Out[47]: 1.00000000000000*x^3 + 2.00000000000000*x^2 + 1.00000000000000

In [48]: p.complex_roots()
Out[48]: 
[[-2.20556943 +/- 1.12e-9] + [+/- 8.60e-10]j,
 [0.10278472 +/- 6.14e-9] + [0.66545695 +/- 2.51e-9]j,
 [0.10278472 +/- 6.14e-9] + [-0.66545695 +/- 2.51e-9]j]

It is usually better when computing roots to have an exact representation of the coefficients but compute approximate representations of the roots:

In [65]: x = flint.fmpz_poly([0, 1])

In [66]: p = (x**2 + 2)**2

In [67]: p
Out[67]: x^4 + 4*x^2 + 4

In [68]: p.complex_roots()
Out[68]: [([1.41421356237310 +/- 4.96e-15]j, 2), ([-1.41421356237310 +/- 4.96e-15]j, 2)]

The multiplicities of the roots are computed exactly but the values of the roots are computed approximately with hard error bounds. There is also much functionality available in python-flint for doing exact things with fmpz_poly etc.

You can compute integrals numerically with hard error bounds:

In [5]: flint.acb.integral(lambda e, _: e.sin(), 0, 1)
Out[5]: [0.459697694131860 +/- 6.21e-16]

The general property that Arb provides is that the true values are always bounded and that this property is preserved under composition of any Arb operations. You could e.g. compute numeric integrals to get the elements of a matrix and then compute the determinant of that matrix and so on and then at the end you still have total bounds on the correct result of the whole operation. You also have a guarantee that if you don’t mind burning more CPU time you can just crank up prec and rerun everything to make the outputs as precise as you want.

The current python-flint interfaces can certainly be improved but a lot of the useful functionality of Arb is available and I think that using these things is really quite game changing in many applications. Even if you don’t generally want to use Arb in your library etc having the ability to use it sometimes to verify other calculations is valuable. Ideally it should be easy to switch between doing a calculation with e.g. numpy or doing it with Arb and then you would have an easy way of checking accuracy. In many applications it might be too slow for normal use but when you find yourself debugging accuracy problems being able to just flip a switch and have fully rigorous numerics is very useful.

2 Likes

FYI, on WIndows (10) installation failed for me. It appeared to be trying to build a binary wheel, and died when Microsoft’s Visual C++ couldn’t find include file gmp.h while compiling src/flint/pyflint.c, The file pip downloaded was

python-flint-0.6.0.tar.gz

This command worked instead, trying what’s probably a later alpha release:

pip install python-flint==0.7.0a5

I didn’t find a relevant issue on python-flint’s issue tracker, so on the chance that I just did something dumb I’m blind to, thought I’d mention it here first :smiley:.

Here’s an interesting (to me) one: Python, mpmath, and flint all return pretty much the same gibberish result for sin(1e50):

>>> import mpmath
>>> mpmath.sin(1e50)
mpf('-0.4805001434937588')
>>> mpmath.sin(mpmath.mpf(1e50))
mpf('-0.4805001434937588')
>>> import flint
>>> flint.arb(1e50).sin()
[-0.480500143493759 +/- 2.55e-16]
>>> import math
>>> math.sin(1e50)
-0.4805001434937588

“The right” answer is -0.78967249342931008271. Except it’s really not :wink: This is an unusual case of “representation error”. The decimal 1e50 isn’t actually representable as a binary float, and Python is actually passing the decimal 100000000000000007629769841091887003294964970946560.

Pass 10**50 instead, and all is fine:

>>> import mpmath
>>> mpmath.sin(10**50)
mpf('-0.78967249342931012')
>>> import flint
>>> flint.arb(10**50).sin()
[-0.789672493429310 +/- 1.21e-16]

The “modern” Windows calculator app also gets the right result for input 1e50, but, as explained before, it uses unbounded rationals under the covers, and literals suffer no representation errors: what you type is exactly what you get.

In the good old days, it was rare to find any program that could compute sin(10**50) correctly. Almost everything did trig argument reduction using an approximation of pi good to only a few dozen (at most) digits. Results were garbage. But familiar garbage, so people fought it when accurate argument reduction was introduced :roll_eyes: