Skip to content

validated_integ3 drops the remainder each step inherits from the previous steps #208

Closed

Description

@milmanb

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

  1. lbenet commented on Oct 11, 2026

    @lbenet
    Member

    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.

  2. lbenet commented on Oct 11, 2026

    @lbenet
    Member

    Note: see the tests in #206, which fix this issue (changing a lot of other things)

  3. milmanb commented on Oct 12, 2026

    @milmanb
    Author

    Thanks for the quick fix! Fine to close this once #206 is merged.

  4. lbenet commented on Oct 12, 2026

    @lbenet
    Member

    It was truly not a quick fix, but something that was already on its way for some time 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions