Numerics and evaluation API
Compiled evaluation, batch evaluation, and ball arithmetic.
Compiled evaluation
Compile a symbolic expression to a fast native function.
Returns a callable Python object (PyCompiledFn).
expected_evals, when given, is the number of evaluations the caller plans
(e.g. the length of the arrays it will pass to numpy_eval); it lets a
small expression that would otherwise get the interpreter be compiled
natively up front. Without it, the first large batch call upgrades the
function on its own (see PyCompiledFn::batch_fn).
Given a list (or tuple) of expressions instead of one, returns a
CompiledVecFn evaluating all of them in one pass per point, with the
subexpressions they share computed once — the shape of a gradient or a
Jacobian. numpy_eval on it returns an array with one row per output.
Numerically evaluate expr; accepts DerivedResult as expr.
Raises DomainError (a ValueError) if expr is
mathematically undefined at bindings — e.g. a removable singularity
such as (x**2 - 1) / (x - 1) evaluated as written at x = 1
reduces to 0/0 and raises, rather than silently returning the limit
(2). Call cancel() first to evaluate the simplified,
everywhere-defined form instead.
Vectorised evaluation of a CompiledFn over arrays.
Phase 25 / PA-8 — NumPy / JAX / PyTorch / DLPack array evaluation.
Evaluation runs entirely natively: input arrays are read via the buffer
protocol (a single bulk copy per array, not a Python float per
element), the compiled function is called with the GIL released, and the
result is written directly into a preallocated output array. No
intermediate Python list of floats (.tolist()) is ever created, which
is what previously made this path dramatically slower than it needed to
be for large arrays.
Two things are easy to get wrong, and both now raise an error that says so:
- the first argument is the compiled function, not the expression —
f = compile_expr(expr, [x, y]), thennumpy_eval(f, xs, ys); - the arrays are separate positional arguments, one per input variable
—
numpy_eval(f, xs, ys), notnumpy_eval(f, [xs, ys]).
(CompiledFn.__call__() goes the other way: it evaluates a single
point, and takes that point as one sequence — f([1.0, 2.0]).)
compiled_fnCompiledFnA compiled function returned by
compile_expr()orCompileCache. Not anExpr, and not a traced Python function.*arraysarray-likeOne array per input variable, passed as separate positional arguments. All arrays must have the same number of elements. Accepts NumPy arrays, PyTorch CPU tensors, JAX arrays, CuPy arrays (host copy made), or anything with
__dlpack__or__array__.
- numpy.ndarray
Output values with the same shape as the first input array.
- TypeError
compiled_fn is not a compiled function — most often an
Exprthat has not been throughcompile_expr().- ValueError
The number of arrays does not match the compiled function's arity, including the common case of one list holding all of them.
>>> import numpy as np
>>> import alkahest
>>> p = alkahest.ExprPool()
>>> x, y = p.symbol("x"), p.symbol("y")
>>> f = alkahest.compile_expr(x ** 2 + y, [x, y])
>>> xs = np.linspace(0, 1, 1_000_000)
>>> ys = np.linspace(1, 2, 1_000_000)
>>> out = alkahest.numpy_eval(f, xs, ys) # one argument per variableParallel vectorised evaluation of a CompiledFn over arrays.
Identical to numpy_eval() but distributes the N evaluation points
across all available CPU cores using Rayon. The Python GIL is released
during evaluation so other threads are not blocked.
Requires the parallel build feature
(maturin develop --features parallel). Falls back silently to the
sequential numpy_eval() when the feature is not compiled in.
compiled_fnCompiledFnA compiled function returned by
compile_expr()orCompileCache— not anExpr.*arraysarray-likeOne array per input variable, passed as separate positional arguments (
numpy_eval_par(f, xs, ys), notnumpy_eval_par(f, [xs, ys])). All arrays must have the same number of elements. Accepts NumPy arrays, PyTorch CPU tensors, JAX arrays, or anything with__dlpack__or__array__.
- numpy.ndarray
Output values with the same shape as the first input array.
For small N (< 1 000 points) thread-scheduling overhead may exceed the
computation time; [/api/numerics/#numpy_eval) is faster in that regime.numpy_eval()](
>>> import numpy as np
>>> import alkahest
>>> p = alkahest.ExprPool()
>>> x = p.symbol("x")
>>> f = alkahest.compile_expr(x ** 2, [x])
>>> xs = np.linspace(0, 1, 10_000_000)
>>> ys = alkahest.numpy_eval_par(f, xs) # multi-core evaluationSeveral outputs compiled together: compile_expr([e1, e2, …], inputs).
Calling it on one point returns a list with one value per output;
numpy_eval(f, xs, ys) returns an array of shape (n_outputs, *xs.shape).
Subexpressions the outputs share are evaluated once per point, and every
output equals what compile_expr of that expression alone returns.
A compiled function returned by compile_expr().
Evaluate at one point, given as a single sequence of floats.
f([1.0, 2.0]) — one argument holding every input value, not one
argument per input. To evaluate over arrays of points instead, use
alkahest.numpy_eval(f, xs, ys), which takes the arrays as separate
positional arguments.
Batch-evaluate over N points (Phase 25 — NumPy/JAX array evaluation).
inputs_flat is a flat list of length n_vars * n_points laid out
var-major: [x0[0], x0[1], …, x1[0], x1[1], …].
Returns a flat list of N outputs. In Python this is wrapped by the
alkahest.numpy_eval helper that handles the buffer-protocol conversion.
Parallel batch evaluation using Rayon (requires --features parallel).
Identical to call_batch_raw() but distributes the N points across
all available CPU cores. The GIL is released during evaluation so other
Python threads are not blocked.
Use numpy_eval_par() for a NumPy-friendly wrapper.
Fast-path batch evaluation directly against buffer-protocol objects
(NumPy float64 arrays, array.array('d', …), memoryviews, …).
Unlike call_batch_raw(), this never materialises a Python list
or float object per element: each input array's contents are copied
into a native Vec<f64> in one bulk memcpy-style operation (via
PyO3's buffer protocol), the compiled function runs with the GIL
released, and the result is written back into the caller-supplied
output buffer the same way.
inputs must contain exactly n_inputs buffers, each of length
n_points, C-contiguous, and holding float64 elements. output
must be a writable buffer of the same shape (e.g. numpy.empty).
This is the method used internally by numpy_eval();
most callers should use that helper rather than calling this
directly.
Parallel counterpart to call_batch_buffer() (requires
--features parallel). Distributes the n_points evaluations across
all available CPU cores via Rayon; the GIL is released for the
duration of the native call.
Used internally by numpy_eval_par().
Emit a C function that evaluates the Horner form of a polynomial.
Returns a str containing a complete C function definition.
exprExprA univariate polynomial in
var.varExpr or list[Expr]The polynomial variable.
emit_conly supports univariate polynomials, so a list/tuple is accepted as a convenience but must contain exactly oneExpr(e.g.[x]is equivalent tox).var_namestrThe C variable name (default
"x").fn_namestrThe C function name (default
"eval_poly").
Ball arithmetic
Create a real ball [mid ± rad].
Midpoint rounded to the nearest f64.
This is a lossy readout: past ~16 digits mid ± rad read through
the f64 accessors is not an enclosure. Use mid_str,
mid_exact or to_mpf for the full-precision midpoint, and
lo / hi for outward-rounded f64 bounds.
Radius, rounded up to f64.
Nearest-rounding here would report a radius smaller than the true one and turn a valid enclosure into an invalid one at the Python boundary.
Lower endpoint, rounded down to f64.
The ball is computed at prec bits — far finer than f64 — so a
nearest-rounded endpoint can land inside the true interval. A caller
writing lo <= v <= hi would then get False for a value the ball
genuinely encloses, which defeats the entire purpose of rigorous
arithmetic. Rounding outward keeps the f64 view a valid enclosure of
the exact one, at the cost of being at most one ulp wider.
Midpoint as a decimal string carrying every digit of the stored
binary value (round-trips exactly), e.g. "1.2020569...e0".
(mid, rad) as mpmath.mpf values, exact conversions of the stored
binary floats (requires mpmath).
Evaluate a symbolic expression using rigorous interval (ball) arithmetic.
bindings is a dict mapping Expr → ArbBall.
GPU evaluation
alkahest.compile_cuda(expr, inputs) -> CudaCompiledFn
Compile a symbolic expression to a CUDA GPU kernel targeting the NVPTX
backend. Returns a CudaCompiledFn object whose .ptx attribute contains
the generated PTX assembly.
Evaluation
No docstring.
The result of evaluating a ModularRecurrence, with the
evidence that makes its residues trustworthy.
singular_indices is the field to read when a residue is surprising:
it lists the steps where the recurrence's leading coefficient was not a
unit mod p and the working precision had to absorb the loss.
K >= k — the precision the forward pass actually ran at.
working_precision - precision is the total p-adic precision lost
to singular steps, and is 0 for a recurrence whose leading
coefficient is a unit throughout.
guess_relation(
constants,
precision_bits=664,
max_abs_coeff=None,
check_precision=True,
*,
digits=None
)
Search for integers aᵢ with Σ aᵢ·constantsᵢ ≈ 0.
Returns the coefficients as a list of Python int (of any size), or
None when no relation is found.
Precision travels with the input. The search runs at the smaller of
precision_bits and the precision the constants carry, and its detection
threshold is derived from that effective width:
================================== ==================================
Input Search precision it allows
================================== ==================================
float 53 bits (the 64-bit engine minimum)
mpmath.mpf its mantissa, passed exactly —
never through float
decimal str / Decimal its significant digits
ArbBall what its radius leaves of 53 bits
int, Fraction unbounded (exact)
digits= a further cap
================================== ==================================
Searching wider than the data used to zero-pad it into exact rationals, which
both hid true relations — 90·ζ(4) = π⁴ from 115-digit strings came back
None under the 664-bit default — and manufactured false ones among the
padded values. An mpf went through float(), so 120-digit inputs were
searched as doubles.
Raises PslqError (E-PSLQ-004) when the relation found is larger
than the inputs' precision can justify, or does not hold to the precision
they carry, rather than returning a number that is an artifact of how the
inputs were written; and E-PSLQ-005 when the inputs are exact rationals
and the relation is simply false for them:
guess_relation([float(pi), float(e), float(log(2))])
# was: [-60771139, 67263243, 11653676] ← confident and meaningless
# now: None, or E-PSLQ-004 — never that relationThe test is on the result, not the input, so small relations among floats
still come back: [1.0, 2.0, 3.0] needs almost no precision to pin down
and is returned normally. See relation_confidence() for the numbers.
A relation among decimal strings or mpf values that passes those tests
is returned, but unendorsed unless digits= declares how many digits
are trustworthy — relation_confidence() reports credible: None
for it until then. digits= also caps the search width:
guess_relation([mpf(x) for x in floats], digits=16) # raises E-PSLQ-004
guess_relation(two_hundred_digit_strings, digits=200) # judged, and passesPass check_precision=False to accept a relation among the supplied
values themselves (the search width is still capped at their precision).
relation_confidence(
constants,
coeffs,
*,
digits=None,
precision_bits=None,
margin_digits=None
) → dict
Judge a found relation against the precision its inputs actually carry.
Pinning down n coefficients bounded by H selects one of the
(2H+1)ⁿ integer vectors in that box, so it takes about
n·log10(2H+1) digits of agreement. When that exceeds the digits the
inputs have, the relation was purchasable from the available precision —
the search would have found something whatever the constants were — and it
is evidence of nothing. Because PSLQ returns the smallest relation the
precision can buy, a purchased one lands just under the bound rather than
over it, so a relation must clear it by margin_digits (default 10) to
count as credible.
The relation is also evaluated. Σ aᵢ·cᵢ is computed exactly on the
values supplied (a float or mpmath.mpf is an exact binary rational,
a decimal string an exact decimal one). It must cancel to the precision the
inputs carry — residual_digits — and the digits it holds to beyond what
the coefficient size explains — excess_digits — must reach
margin_digits. A relation that fails either test is credible: False
whatever the declared precision: a cheap relation that does not hold is not
rescued by being cheap.
The input's precision has to be knowable, and often it is not. A
float is 53 bits however it is printed, so it is judged. int and
Fraction are exactly the rationals they spell, so precision cannot be
the reason a relation among them is spurious — instead the relation itself
is evaluated (see exact_residual below). A decimal string is
unknown: the digits "3.14159265358979" are equally the exact rational
314159265358979/10¹⁴ and π truncated to 15 places, and nothing in the string
says which. An mpmath.mpf is unknown for the same reason: its accuracy
is not recoverable from the object. Unknown is not unbounded, though: a
literal's length is a ceiling on what it can carry (a 20-digit string
holds at most 20 digits, an mpf at most its mantissa's bits), which is
enough to refute a relation. Pass digits= (decimal) or
precision_bits= (binary) to say how many digits are trustworthy; a
declaration is a cap, so declaring 200 digits for a float still yields
~16, and for a 20-digit string still yields 20.
Returns a dict:
credible
True, False, or None for unknown — the precision of the
inputs could not be established, so no judgement was made. None is
never a pass: treat it as "not yet checked" and re-run with digits=.
A relation can still be refuted (False) with the precision unknown.
available_digits
Digits the inputs are known to carry (inf for exact inputs),
or None when unknown.
consumed_digits
Digits of agreement the relation costs, n·log10(2H+1).
spare_digits
available - consumed, or None when unknown.
margin_digits
Spare digits demanded of a credible relation.
precision_source
Where available_digits came from: "float", "exact",
"declared", "content" (a declaration capped by the length of
the literals), "ball", or "unknown".
residual_digits
Digits to which Σ aᵢ·cᵢ cancels relative to Σ |aᵢ·cᵢ|
(inf for an exact zero), or None when it cannot be evaluated.
excess_digits
min(residual_digits, ceiling) - consumed_digits: agreement the
coefficient size does not explain. A credible relation has at least
margin_digits of it.
exact_residual
Σ aᵢ·cᵢ as an exact Fraction when every constant
is an exact rational and no precision was declared, otherwise None.
A nonzero value refutes the relation, and is the one way
credible can be False while available_digits is inf:
nothing is short of precision, the relation is just false.
Note what credible: True means on the exact branch: the relation holds
for the numbers supplied. If those numbers are rounded stand-ins for
something else — Fraction(*float(pi).as_integer_ratio()) is exactly the
double, not π — then so is the verdict, and no gate reading only the
constants can tell. Declare digits= when they are stand-ins; that is
what turns them back into approximations with a precision to be judged.
>>> import alkahest as ak
>>> ak.relation_confidence([1.0, 2.0, 3.0], [1, 1, -1])["credible"]
True
>>> ak.relation_confidence([0.1, 0.2, 0.7], [60771139, 67263243, 11653676])["credible"]
FalseExact inputs are judged by evaluating the relation, not by counting digits:
>>> ak.relation_confidence([1, 2, 3], [1, 1, -1])["credible"]
True
>>> ak.relation_confidence([1, 2, 3], [1, 1, 1])["credible"]
FalseA decimal string is not endorsed until its precision is declared, but a relation that does not hold is refuted either way:
>>> sqrt2 = "1.41421356237309504880168872420969807856967187537694806841301"
>>> two_sqrt2 = "2.82842712474619009760337744841939615713934375075389613682602"
>>> print(ak.relation_confidence([sqrt2, two_sqrt2], [-2, 1])["credible"])
None
>>> ak.relation_confidence([sqrt2, two_sqrt2], [-2, 1], digits=60)["credible"]
True
>>> ak.relation_confidence([sqrt2, two_sqrt2], [-3, 1], digits=60)["credible"]
FalseA ten-digit-per-coefficient relation among 20-digit strings is exactly what 20 digits can buy, and declaring more digits than the strings hold does not change that:
>>> pi_20 = "3.1415926535897932385"
>>> e_20 = "2.7182818284590452354"
>>> big = [5144503108, -5945642943]
>>> ak.relation_confidence([pi_20, e_20], big, digits=60)["credible"]
FalseRigorous bounds
alkahest.bound_on_box(expr, box, *, order=6, prec=128, tol=1e-9, max_subdivisions=2048)
Rigorous enclosure of the range of expr over an axis-aligned box,
via Taylor models plus Moore–Skelboe branch-and-bound.
box is a list of (variable, lo, hi). The returned
Enclosure is a guaranteed outer bound; when the work budget runs
out it is returned anyway with budget_exhausted=True — wide but true.
alkahest.bounds_supported(expr) -> BoundsSupport
Can the validated-bounds subsystem bound this expression at all?
bound_on_box(), verified_integral(), verified_no_roots()
and verified_sign() all evaluate through the same Taylor-model
evaluator, and it refuses any construct it has no rigorous rule for with
E-VALIDATED-001. This answers that question without running the
bound, so a planning loop can pick a certifiable route instead of
discovering the boundary by hitting it.
The answer is produced by running the real evaluator on a probe box, so it
cannot drift from what bound_on_box() does. The same information per
primitive is the taylor_model flag in
capabilities()["primitives"].
What it does not promise. True means no E-VALIDATED-001. A
supported function can still be refused on a particular box for a
domain violation (E-VALIDATED-003, e.g. log on [-2, -1]) or a
non-finite enclosure (E-VALIDATED-004) — those depend on the box, not
on the expression, so no box-free predicate can rule them out.
Note this is a different question from certifiable(), which
asks whether an operation emits a Lean certificate. A rigorous
enclosure is not a Lean proof term, and the validated subsystem is not in
the certificate ledger; conflating the two under one predicate would make
a True mean two different kinds of evidence.
>>> import alkahest as ak
>>> p = ak.ExprPool(); x = p.symbol("x")
>>> bool(ak.bounds_supported(ak.sin(x) * ak.exp(x)))
True
>>> bool(ak.bounds_supported(ak.bessel_j0(x))) # covered since 3.9.0
True
>>> answer = ak.bounds_supported(ak.floor(x))
>>> bool(answer), answer.functions
(False, ['floor'])Whether the validated-bounds subsystem can bound an expression at all.
Returned by bounds_supported(). Truthy exactly when every
construct in the expression has a rigorous Taylor-model rule, so it drops
into if ak.bounds_supported(f):.
supportedboolThe verdict.
bool(self)is the same value.blockerstr or NoneThe evaluator's own description of the first construct it has no rule for (
"function `bessel_j0`"), orNonewhen supported.functionslist of strEvery function in the expression with no Taylor-model rule, sorted. Empty when
supportedisTrue— and possibly empty when it isFalse, if the blocker is a node kind rather than a function.detailstrOne-sentence human explanation.
A rigorous enclosure: the true value is guaranteed to lie in
[lower, upper].
Returned by bound_on_box() and
verified_integral(). The bound may be wide, but it is
never wrong — that is the whole contract. Check
budget_exhausted to see whether it is as tight as you asked for.
verified_integral(
expr,
var,
a,
b,
*,
order=8,
prec=128,
tol=1e-9,
max_subdivisions=4096
)
alkahest.verified_integral(expr, var, a, b, *, order=8, prec=128, tol=1e-9, max_subdivisions=4096)
Rigorous enclosure of ∫_a^b expr d(var) by Taylor-model quadrature.
Unlike integrate_definite(), this never needs a closed form — and
unlike a floating-point quadrature, the answer is a theorem: the true
integral is guaranteed to lie in the returned interval. Refuses on
singular or improper integrands rather than guessing.
A removable singularity is not a refusal: an integrand written as
N(x)/D(x) whose two halves vanish at the same point p, to the same
order, with some D**(d)(p) != 0 — log(1+x)/x on [0, 1],
sin(x)/x on [-1, 1], (1-cos(x))/(x*x) on [0, 1] — is
enclosed via Cauchy's mean value theorem, iterated as many times as the
order of the zero requires, and the value returned is the integral of the
continuous extension. The zeros are checked symbolically, so a genuine
pole is never mistaken for a removable one.
A bounded integrand whose Taylor model runs out of domain rather than
out of function is not a refusal either. asin and acos on
[0, 1], sqrt(1 - x*x) on [0, 1], sqrt(x) and x**x on
[0, 1] are all finite and continuous on the closed interval, but the
Taylor rule needs a derivative bound it does not have at the end of the
domain, so no model exists on the last panel however far it is bisected.
That panel is closed with a width * range bound computed by interval
arithmetic, which needs no derivative. The panel is of order 2**-60 of
the interval by then, so the crude bound costs essentially nothing.
An unbounded integrand is still refused, whether or not the integral
converges (-log(x) on [0, 1], 1/sqrt(1-x*x) on [0, 1]): the
integral exists, but no rigorous enclosure of the integrand does, and no
bounded range exists either. The ValidatedError message names the
Taylor rule that stopped and where, and does not claim the integral fails
to exist.
verified_no_roots(
expr,
box,
*,
order=6,
prec=128,
tol=1e-9,
max_subdivisions=2048
)
alkahest.verified_no_roots(expr, box, ...) -> "true" | "false" | "undecided"
Three-valued: "true" proves expr has no root anywhere in the box,
"false" proves it does, and "undecided" means neither could be
established within the budget. The third is never collapsed into the
other two.
"false" is certified by the intermediate value theorem: the box is
first proven free of poles and branch cuts (so expr is continuous on it),
then the box is subdivided until two points are found at which expr is
rigorously proven to have opposite signs. A box is convex, so the segment
joining them stays inside it and expr must vanish somewhere along it.
Because the two points need not be the box's own endpoints, an even number
of roots no longer defeats the test — x*x - 2 on [-2, 2] is
"false".
A root that never produces a sign change — a double root such as
(x-1)**2 — stays "undecided": no witness exists, and none is
invented.
verified_sign(
expr,
box,
predicate,
*,
order=6,
prec=128,
tol=1e-9,
max_subdivisions=2048
)
alkahest.verified_sign(expr, box, predicate, ...) -> "true" | "false" | "undecided"
predicate is one of "positive", "negative", "nonnegative",
"nonpositive". A "false" verdict is itself certified — either the
enclosure proves the predicate fails everywhere, or a rigorously evaluated
point witnesses the failure.
An inequality that is tight at an endpoint of the box is still decided.
Subdivision alone cannot do it — where the margin vanishes, every enclosure
of the range straddles zero — so the box is split: a collar at the endpoint
is handled by a truncated Taylor expansion with a proven Lagrange remainder,
the rest by branch-and-bound. That covers the classical sharp trigonometric
inequalities (Cusa–Huygens, Mitrinović–Adamović, Wilker, Huygens, Jordan) on
boxes reaching x = 0. Tightness in the interior is not covered and
stays "undecided".
"undecided" is not monotone in the box: it is a search verdict, not
a logical one, and a box that stops just short of the point an inequality is
tight at gets a much weaker collar than one that reaches it. A box that
stops short of x = 0 is retried with the collar planted at 0 — a
"true" on the larger box implies "true" on the box asked about — so
the classical inequalities decide on [1e-12, pi/2] as well as on
[0, pi/2]. For a statement tight somewhere else the effect remains: on
"undecided", try the larger box whose endpoint is the tight point
before concluding the statement is out of reach.
tol sets the tolerance of the enclosure, not of the verdict: it is an
absolute width, so it does not bound how close to zero the answer may be.
Once the enclosure has been computed, the search is re-run with the sign
itself as the stopping rule, which is what decides an inequality whose
margin is narrower than tol.
verified_ode(
rhs,
vars,
t,
y0,
t_span,
*,
order=20,
prec=128,
tol=1e-16,
max_steps=20000,
h_min=None,
max_rel_width=1.0
)
alkahest.verified_ode(rhs, vars, t, y0, t_span, *, order=20, prec=128, tol=1e-16, max_steps=20000, h_min=None, max_rel_width=1.0)
Validated integration of y' = f(t, y), y(t0) ∈ y0: returns
boxes that are guaranteed to contain the true solution (every solution,
when y0 is a box), or refuses with a coded ValidatedError.
Method: interval Taylor series of order order (coefficients by
automatic differentiation of rhs in ball arithmetic), an a-priori
enclosure per step validated by the Picard–Lindelöf operator, a Lagrange
remainder bound, and Lohner's QR coordinate frame against the wrapping
effect.
rhslist of Expr —
f_i(t, y).varslist of state symbols
y_i(same length asrhs).tthe time symbol, or
Nonefor an autonomous system.y0initial values — each a float (a point) or a
(lo, hi)pair.t_span(t0, t1)witht0 < t1.orderTaylor order per step (1..60).
precworking precision in bits.
tollocal error target (relative to
max(1, |y|)); only steers the step size, never soundness.max_stepsrefuse with
E-VALIDATED-008beyond this many steps.h_minsmallest step tried (default
1e-12 * (t1 - t0)).max_rel_widthrefuse with
E-VALIDATED-007when a component's width exceeds this timesmax(1, |y|).
- Example
:
y = pool.symbol("y") sol = ak.verified_ode([-y], [y], None, [1.0], (0.0, 1.0)) lo, hi = sol.enclosure(1.0)[0] # contains exp(-1), width ~1e-25
Rigorous enclosures of an ODE solution, returned by
verified_ode().
times[i] is an exact grid point and enclosures[i] a list of
(lo, hi) intervals, one per state component, that is guaranteed to
contain y(times[i]). Bounds are rounded outward to float.
enclosure() encloses the solution at any t in the span.
Grid enclosures: enclosures[i][c] = (lo, hi) contains component
c of the solution at times[i].