arb_fpwrap.hUsing the floating-point wrappers
The functions of arb_fpwrap.h are meant for programs that work with double numbers and want special functions with a guaranteed accuracy, without using ball arithmetic themselves. Every function has the form
int arb_fpwrap_double_NAME(double * res, double x, ..., int flags);
int arb_fpwrap_cdouble_NAME(complex_double * res, complex_double x, ..., int flags);
It writes the result to res and returns FPWRAP_SUCCESS (0) or, if it cannot reach the required accuracy (for example at a pole, outside the real domain of a real function, or when the work limit is reached), FPWRAP_UNABLE (1) with a NaN result. Internally the function is evaluated in ball arithmetic at 64 bits, then at 128 bits, and so on, until the enclosure determines the double result; a successful result is therefore never wrong by more than one unit in the last place, and with FPWRAP_CORRECT_ROUNDING it is correctly rounded.
Calling from C#
#include <stdio.h>
#include <flint/arb_fpwrap.h>
int main(void)
{
double x, y;
complex_double cx, cy;
int status;
x = 2.0;
status = arb_fpwrap_double_zeta(&y, x, 0);
printf("zeta(%g) = %.16g (status %d)\n", x, y, status);
cx.real = 0.5;
cx.imag = 123.0;
status = arb_fpwrap_cdouble_zeta(&cy, cx, 0);
printf("zeta(%g + %gi) = %.16g + %.16gi (status %d)\n",
cx.real, cx.imag, cy.real, cy.imag, status);
/* a pole: no finite result */
status = arb_fpwrap_double_zeta(&y, 1.0, 0);
printf("zeta(1): status %d, result is NaN: %d\n", status, y != y);
/* cancellation: sin(2^60) needs extra precision internally */
status = arb_fpwrap_double_sin(&y, 1152921504606846976.0, FPWRAP_CORRECT_ROUNDING);
printf("sin(2^60) = %.17g (status %d)\n", y, status);
/* the 1000th zero of the zeta function */
status = arb_fpwrap_cdouble_zeta_zero(&cy, 1000, 0);
printf("rho_1000 = %.16g + %.16gi (status %d)\n", cy.real, cy.imag, status);
flint_cleanup();
return 0;
}zeta(2) = 1.644934066848226 (status 0) zeta(0.5 + 123i) = 0.006252861175594465 + 0.08206030514520983i (status 0) zeta(1): status 1, result is NaN: 1 sin(2^60) = -0.83064921763725463 (status 0) rho_1000 = 0.5 + 1419.422480945996i (status 0)
The wrappers cache constants and other data as the ball functions do; flint_cleanup frees these caches at the end of the program (this is only needed to make memory checkers quiet). The functions are thread-safe.
The flags argument combines FPWRAP_ACCURATE_PARTS (accurate real and imaginary parts separately), FPWRAP_CORRECT_ROUNDING and a multiple of FPWRAP_WORK_LIMIT (the maximal working precision). For example, FPWRAP_CORRECT_ROUNDING + 10 * FPWRAP_WORK_LIMIT asks for a correctly rounded result and allows working precisions up to bits.
Calling from Python#
The functions can be called from Python with ctypes; since FLINT 3.0 they are part of libflint (in Arb they were in libarb). The following snippet is not tested automatically:
import ctypes
import ctypes.util
libflint = ctypes.CDLL(ctypes.util.find_library('flint'))
class _complex_double(ctypes.Structure):
_fields_ = [('real', ctypes.c_double),
('imag', ctypes.c_double)]
def wrap_double_fun(fun):
def f(x):
y = ctypes.c_double()
if fun(ctypes.byref(y), ctypes.c_double(x), 0):
raise ValueError(f"unable to evaluate function accurately at {x}")
return y.value
return f
def wrap_cdouble_fun(fun):
fun.argtypes = [ctypes.POINTER(_complex_double), _complex_double, ctypes.c_int]
def f(x):
x = complex(x)
cx = _complex_double(x.real, x.imag)
cy = _complex_double()
if fun(ctypes.byref(cy), cx, 0):
raise ValueError(f"unable to evaluate function accurately at {x}")
return complex(cy.real, cy.imag)
return f
zeta = wrap_double_fun(libflint.arb_fpwrap_double_zeta)
czeta = wrap_cdouble_fun(libflint.arb_fpwrap_cdouble_zeta)
print(zeta(2.0)) # 1.6449340668482264
print(czeta(0.5+1e9j)) # (-2.761748029838061-1.6775122409894598j)
print(zeta(1.0)) # pole: raises ValueError
The package python-flint (see Python interfaces) offers the ball arithmetic directly.
Calling from Julia#
With ccall, the wrappers can be called from Julia; Complex{Float64} has the layout of complex_double (not tested automatically):
function zeta(x::Float64)
cy = Ref{Float64}()
if Bool(ccall((:arb_fpwrap_double_zeta, :libflint), Cint,
(Ptr{Float64}, Float64, Cint), cy, x, 0))
error("unable to evaluate accurately at ", x)
end
return cy[]
end
function zeta(x::Complex{Float64})
cy = Ref{Complex{Float64}}()
if Bool(ccall((:arb_fpwrap_cdouble_zeta, :libflint), Cint,
(Ptr{Complex{Float64}}, Complex{Float64}, Cint), cy, x, 0))
error("unable to evaluate accurately at ", x)
end
return cy[]
end
println(zeta(2.0)) # 1.6449340668482264
println(zeta(0.5 + 1e9im)) # -2.761748029838061 - 1.6775122409894598im
println(zeta(1.0)) # pole: throws an error
Cost#
Most calls succeed at the first precision of 64 bits, and the cost is then that of one evaluation of the ball function at 64 bits, typically between a few hundred nanoseconds for elementary functions and tens of microseconds for hypergeometric functions, which is slower than a floating-point library but without its accuracy problems. Inputs where the function is ill-conditioned (for example near its zeros) and correct rounding of values close to a rounding boundary need higher precisions; each failed attempt doubles the precision, so the total cost is dominated by the last attempt. Large arguments such as in are handled by the ball functions themselves, which add the precision needed for the argument reduction.
The FLINT team, “Using the floating-point wrappers (arb_fpwrap.h),” FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev, 2026. https://flintlib.org/doc/arb_fpwrap/usage.html (generated 2026-11-02).
@misc{flint-doc-arb-fpwrap-usage,
author = {{The FLINT team}},
title = {{Using the floating-point wrappers (arb\_fpwrap.h)}},
howpublished = {FLINT: Fast Library for Number Theory, documentation, version 3.7.0-dev},
year = {2026},
url = {https://flintlib.org/doc/arb_fpwrap/usage.html},
note = {Generated 2026-11-02}
}