Repository navigation
validated_integ3 drops the remainder each step inherits from the previous steps #208
Description
In _validation3! (validated_integ3.jl L277, master at 27c00e2), the initial condition of each step gets a zero remainder:
# Include remainder of initial condition
x1N[i].pol.coeffs[1].rem = z#rem0[i]rem0 holds the remainder of the previous step's end state (rem0 .= remainder.(vTMN), L127). With a zero remainder on the initial condition, the Picard image's remainder, which becomes the step's remainder (x1N[i].rem = rem2[i], L287), does not contain rem0; rem0 enters only the contraction test (rem1 .= remainder.(x1N) .+ rem0, L281). So the enclosure returned for a step does not account for the error accumulated before it. This looks like the validated_integ3 counterpart of cause 1 in #207.
rem0 is nonzero whenever _abs_rems! does not absorb the whole remainder (its residual interval(r_lo, r_hi), or the full remainder when it takes its "do nothing" branch), and it is the whole remainder when absorption is off, which one needs for a function enclosure (Taylor variables that keep addressing their own initial points).
Reproducer (TaylorModels v0.12.0, Julia 1.13.1)
Geodesic flow on the unit sphere, where every initial point of the box has a closed-form solution. Absorption is disabled so that the pointwise check is meaningful; use_fix!(true) re-evaluates the package's own _validation3! with that one line changed to = rem0[i].
using TaylorModels, TaylorModels.ValidatedInteg, IntervalArithmetic, TaylorSeries, Printf
setprecision(BigFloat, 256)
# Geodesic flow on the unit sphere: x' = v, v' = -(|v|^2/|x|^2) x. From x0 = e_z with v0 = (v1, v2, 0), c = |v0|,
# the exact solution is x(t) = cos(c t) e_z + sin(c t) v0 / c, for every point of the initial box below.
function spray!(dx, x, p, t)
q = (x[4]^2 + x[5]^2 + x[6]^2) / (x[1]^2 + x[2]^2 + x[3]^2)
dx[1] = x[4]; dx[2] = x[5]; dx[3] = x[6]
dx[4] = -q * x[1]; dx[5] = -q * x[2]; dx[6] = -q * x[3]
nothing
end
const A, B = 0.7, 0.7 + 2pi / 256
const X0 = [interval(0.0), interval(0.0), interval(1.0),
interval(cos(B), cos(A)), interval(sin(A), sin(B)), interval(0.0)]
# Disable the remainder absorption, so the Taylor variables keep addressing their own initial points
# (a function enclosure); the pointwise check below is only meaningful then.
@eval TaylorModels.ValidatedInteg _abs_rems!(vTMN::Vector{TaylorModelN{N,T,S}}) where {N,T,S} = nothing
# Optionally restore the dropped remainder: the package's own `_validation3!` with one line replaced.
function use_fix!(fix::Bool)
src = split(read(joinpath(pkgdir(TaylorModels), "src", "valid_integ", "validated_integ3.jl"), String), '\n')
i0 = findfirst(l -> startswith(l, "function _validation3!"), src)
i1 = i0 + findfirst(==("end"), src[i0+1:end])
fun = join(src[i0:i1], '\n')
fix && (fun = replace(fun, "x1N[i].pol.coeffs[1].rem = z#rem0[i]" => "x1N[i].pol.coeffs[1].rem = rem0[i]"))
Core.eval(TaylorModels.ValidatedInteg, Meta.parse(fun))
end
function check(label)
sol = validated_integ3(spray!, X0, 0.0, 0.5, 2, 12, 1e-15, JetSpace(4, ["ξ$i" for i in 1:6]);
maxsteps = 1000, parse_eqs = false)
tTM, qTM = expansion_point(sol), get_xTM(sol)
fails, worst = 0, BigFloat(0)
for n in 2:length(sol), a in (-0.9, 0.0, 0.9), b in (-0.9, 0.0, 0.9), fr in (0.25, 0.75)
dom = domain(qTM[1, n]); dt = inf(dom) + fr * (sup(dom) - inf(dom))
xi = interval.([0.0, 0.0, 0.0, a, b, 0.0])
v1 = BigFloat(mid(X0[4])) + a * BigFloat(radius(X0[4]))
v2 = BigFloat(mid(X0[5])) + b * BigFloat(radius(X0[5]))
c, t = sqrt(v1^2 + v2^2), BigFloat(tTM[n]) + BigFloat(dt)
exact = [sin(c * t) * v1 / c, sin(c * t) * v2 / c, cos(c * t)]
for i in 1:3
e = evaluate(evaluate(qTM[i, n], interval(dt)), xi)
excess = max(BigFloat(inf(e)) - exact[i], exact[i] - BigFloat(sup(e)), BigFloat(0))
fails += excess > 0; worst = max(worst, excess)
end
end
@printf("%-14s steps=%d failures=%d/162 worst excess=%.3e remainder(x1) first step %.2e, last step %.2e\n",
label, length(sol) - 1, fails, Float64(worst),
radius(remainder(qTM[1, 2])), radius(remainder(qTM[1, end])))
end
use_fix!(false); check("as released")
use_fix!(true); check("with the fix")Output:
as released steps=3 failures=48/162 worst excess=1.852e-08 remainder(x1) first step 9.90e-09, last step 1.40e-25
with the fix steps=3 failures=0/162 worst excess=0.000e+00 remainder(x1) first step 9.90e-09, last step 3.12e-08
As released, the enclosure misses the exact value at 48 of 162 points (by up to 1.9e-8), and the stored remainder of x[1] shrinks from 9.9e-9 at the first step to 1.4e-25 at the third. With rem0[i] restored every point is enclosed, and the remainder carries the earlier steps.
On the default path (absorption on), I instrumented _abs_rems! on the same problem. At each of the three steps it left two components (x[3] and x[6]) unabsorbed, with remainders up to 8.6e-7 (the largest remainder of the step), which the next step then drops. I did not attempt a pointwise check on that path, since absorption shifts the other components' polynomials.
Suggested fix
Restore the commented-out value: x1N[i].pol.coeffs[1].rem = rem0[i]. The contraction test then counts rem0 twice in rem1, which is conservative and harmless.
Also
validated_integ3 accepts absorb::Bool = false, but the argument is not passed on (it is commented out in the call to _validated_integ3!), and _abs_rems! is called unconditionally, so the keyword has no effect.
Found while testing validated enclosures of geodesic flows; we patched _validation3! at run time as above.
Activity
lbenet commentedon Oct 11, 2026
Thanks for opening this issue; I partially encountered this (in a slightly different context), which has led to #206.
My idea originally was to "reset" the Picard-iteration, in order to approach the fixpoint "from below", but indeed, that misses some cases, i.e., the truncation of the t-expansion, so whenever that's not too small, the problems arise. #206 avoids
_abs_rems!by eploiting thr QR precondition; that PR requires JuliaDiff/TaylorSeries.jl#436, which I think is almost ready but not yet merged. I'll try to adapt your example to this PR to having further tests, but today is complicated.Add a geodesic flow test, and bump TI compat entry
lbenet commentedon Oct 11, 2026
Note: see the tests in #206, which fix this issue (changing a lot of other things)
milmanb commentedon Oct 12, 2026
Thanks for the quick fix! Fine to close this once #206 is merged.
Adapt validated_integ3 to use QR-precondition (instead of abs_rems!) (#…
lbenet commentedon Oct 12, 2026
It was truly not a quick fix, but something that was already on its way for some time 😄