General concepts¶
Importing¶
The flint
module exposes a set of distinctly-named types together
with a small number of top-level functions and objects.
Most functionality is provided as methods on the types. This means
that there should be no namespace conflicts with most user code,
with Python’s builtin math
and cmath
modules, or with
packages such as gmpy
, numpy
, sympy
and mpmath
.
For typical interactive use, it should therefore
generally be safe to import *
:
>>> from flint import *
>>> fmpq(3) / 2
3/2
For non-interactive use, it is still good manners to use explicit
imports or preserve the flint
namespace prefix:
>>> import flint
>>> flint.fmpq(3) / 2
3/2
Global context¶
Various settings are controlled by a global context object,
flint.ctx
. Printing this object in the REPL shows the current
settings, with a brief explanation of each parameter:
>>> from flint import ctx
>>> ctx
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 user can mutate the properties directly, for example:
>>> ctx.pretty = False
>>> fmpq(3,2)
fmpq(3,2)
>>> ctx.pretty = True
3/2
Calling ctx.default()
restores the default settings.
The special method ctx.cleanup()
frees up internal caches
used by MPFR, FLINT and Arb. The user does normally not have to
worry about this.
Types and methods¶
As a general rule, C functions associated with a type in FLINT or Arb are exposed as methods of the corresponding Python type.
For example, there is both an fmpq.bernoulli()
(which computes
a Bernoulli number as an exact fraction) and arb.bernoulli()
(which computes a Bernoulli number as an approximate real number).
A function that transforms a single value to the same type
is usually an ordinary method of that type, for instance arb.exp()
.
A function with a different signature can either provided as a
static method that takes all inputs as function arguments, or as a
method of the “primary” input, taking the other inputs
as arguments to the method (for example arb.bessel_j()
).
When a method involves different types for inputs and outputs (or
just among the inputs), it will
typically be a method of the more “complex” type. For example, a matrix
type is more “complex” than the underlying scalar type, so
fmpz_mat.det()
is a method of the matrix type, returning a scalar,
and not vice versa.
The method-based interface is intended to keep the code simple,
not to be aesthetically pleasing to mathematicians. A functional
top-level interface might be added in the future, allowing more idiomatic
mathematical notation (for example, exp()
and
det()
as regular functions).
Mutability¶
Objects have immutable semantics. For example, the second line in:
b = a
a += c
leaves b unchanged.
However, mutation via direct element access is supported for matrices and polynomials. Some methods also allow explicitly performing the operation in-place. Civilized users will restrict their use of such methods to the point in the code where the object is first constructed:
def create_thing(): # ok
a = thing()
a.mutate()
return a
Crashing and burning¶
Very little overflow checking is done ahead-of-time. Trying to compute an
object far too large to hold in memory (for example, the exact factorial
of \(2^{64}-1\)) will likely abort the process,
instead of raising an OverflowError
or MemoryError
that
can be caught at the Python level.
Input that is obviously invalid (for example a negative number passed as a length) can also cause crashes or worse things to happen. Ideally, bad input should be caught at the Python level and result in appropriate exceptions being raised, but this is not yet done systematically. At this time, users should assume that invalid input leads to undefined behavior!
Inexact numbers and numerical evaluation¶
Real and complex numbers are represented by midpoint-radius intervals (balls). All operations on real and complex numbers output intervals representing rigorous error bounds. This also extends to polynomials and matrices of real and complex numbers.
The working precision for real and complex arithmetic is controlled by the
global context object attributes ctx.prec()
(in bits)
ctx.dps()
(in decimal digits). Changing either attribute changes
the other to match.
Be careful about using Python float and complex literals as input.
Doing arb(0.1)
actually gives an interval containing
the rational number
which might not be what you want. Do arb("0.1")
, arb("1/10")
or arb(fmpq(1,10))
if
you want the correct decimal fraction. Small integers and
power-of-two denominators are still safe, for example arb(100.25)
.
Pointwise boolean predicates (such as the usual comparison operators)
involving inexact numbers return
True only if the predicate certainly is true (i.e. it holds for all
combinations of points that can be chosen from the set-valued inputs),
and return False if the
predicate either definitely is false or the truth cannot be determined.
To determine that a predicate is definitely false,
test both the predicate and the inverse predicate,
e.g. if either x < y
or y <= x
returns True, then the other
is definitely false; if both return False, then neither can be
determined from the available data.
The following convenience functions are provided for numerical evaluation with adaptive working precision.
-
flint.
good
(func, long prec=0, long maxprec=0, long dps=0, long maxdps=0, long padding=10, bool verbose=False, bool show=False, bool parts=True, metric=None)¶ Evaluates func, automatically increasing the precision to get a result accurate to the current working precision (or the precision specified by prec or dps).
>>> good(lambda: (arb.pi() + arb("1e-100")).sin()) Traceback (most recent call last): ... ValueError: no convergence (maxprec=630, try higher maxprec) >>> good(lambda: (arb.pi() + arb("1e-100")).sin(), maxprec=1000) [-1.00000000000000e-100 +/- 3e-119]
The function func can return an arb, an acb, or a composite object such as a tuple or a matrix. By default all real and imaginary parts of all components must be accurate. This means that convergence is not possible in case of inexact zeros. This behavior can be overridden by setting parts to False.
>>> good(lambda: (acb(0,-1) ** 0.5) ** 2) Traceback (most recent call last): ... ValueError: no convergence (maxprec=630, try higher maxprec) >>> good(lambda: (acb(0,-1) ** 0.5) ** 2, parts=False) [+/- 4.50e-22] + [-1.00000000000000 +/- 3e-20]j
-
flint.
showgood
(func, **kwargs)¶ Evaluates func accurately with
good()
, printing the decimal value of the result (without an explicit radius) instead of returning it.>>> showgood(lambda: arb.pi()) 3.14159265358979 >>> showgood(lambda: arb.pi(), dps=50) 3.1415926535897932384626433832795028841971693993751
Power series¶
Power series objects track the precision (the number of known terms)
automatically. The upper precision for power series is controlled by
flint.ctx.cap
, with the default value 10.
>>> fmpq_series([0,1]).exp()
1 + x + 1/2*x^2 + 1/6*x^3 + 1/24*x^4 + 1/120*x^5 + 1/720*x^6 + 1/5040*x^7 + 1/40320*x^8 + 1/362880*x^9 + O(x^10)
>>> ctx.cap = 4
>>> fmpq_series([0,1]).exp()
1 + x + 1/2*x^2 + 1/6*x^3 + O(x^4)
>>> ctx.cap = 10
>>> fmpq_series([0,1], prec=5).exp()
1 + x + 1/2*x^2 + 1/6*x^3 + 1/24*x^4 + O(x^5)
>>> ctx.cap = 3
>>> ctx.dps = 10
>>> arb_series([1,3,4]).exp()
([2.718281828 +/- 4.79e-10]) + ([8.154845485 +/- 4.36e-10])*x + ([23.10539554 +/- 2.25e-9])*x^2 + O(x^3)
>>> ctx.default()