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

\[3602879701896397 \times 2^{-55} = 0.1000000000000000055511151231257827021181583404541015625\]

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()