Quickstart

DFMethods plugs into NonlinearSolve.jl's standard solve interface. After using DFMethods, solve(prob, alg) works exactly as for any SciML algorithm. The algorithm itself carries no constraint information; the feasible set is a property of the problem (see Constraint Sets).

Out-of-place problem

# F : R^n → R^n  (out-of-place — returns a new vector)
F(u, p) = u .- sin.(u)

n    = 1000
x0   = ones(n)
prob = NonlinearProblem(F, x0)

alg = DFProjection()              # all defaults
sol = solve(prob, alg)
sol.retcode
ReturnCode.Success = 1

The returned solution exposes the iterate, residual, and statistics:

(retcode = sol.retcode,
 iters   = sol.stats.nsteps,
 nf      = sol.stats.nf,
 resid_norm = sqrt(sum(abs2, sol.resid)))
(retcode = SciMLBase.ReturnCode.Success, iters = 18, nf = 40, resid_norm = 4.333497786962921e-7)

In-place problem

The same NonlinearProblem constructor accepts in-place mappings; DFMethods handles both forms automatically.

function F!(du, u, p)
    @. du = u - sin(u)
    return nothing
end

sol_ip = solve(NonlinearProblem(F!, ones(1000)), DFProjection())
sol_ip.retcode
ReturnCode.Success = 1

Box constraints

Box bounds are passed to the problem via the SciML-standard lb / ub keyword arguments. The algorithm requires no change.

prob_box = NonlinearProblem(F, ones(100); lb = fill(-2.0, 100), ub = fill(2.0, 100))
sol_box  = solve(prob_box, DFProjection())
sol_box.retcode
ReturnCode.Success = 1

Other convex constraints

For arbitrary closed convex sets, wrap the problem in ConstrainedNonlinearProblem:

inner    = NonlinearProblem(F, ones(100))
prob_hs  = ConstrainedNonlinearProblem(inner, HalfSpace(ones(100), 50.0))
sol_hs   = solve(prob_hs, DFProjection())
sol_hs.retcode
ReturnCode.Success = 1

See Constraint Sets for the full menu of built-in sets.

Parameters

NonlinearSolve.jl's p argument flows through unchanged:

G(u, p) = u .- p
prob_p  = NonlinearProblem(G, [1.0, -1.0], [0.3, -0.2])
sol_p   = solve(prob_p, DFProjection())
sol_p.u ≈ [0.3, -0.2]
false

Overriding tolerances

abstol and maxiters are passed through SciML's standard kwargs and override the algorithm's defaults:

sol_tight = solve(prob, DFProjection(); abstol = 1e-10, maxiters = 5000)
sol_tight.retcode
ReturnCode.Success = 1
alg_alt = DFProjection(;
    direction  = SpectralThreeTerm(; r = 0.05),
    linesearch = ConstantBacktrack(; σ = 1e-4, ρ = 0.5),
    inertial   = NoInertial(),
)
sol_alt = solve(prob, alg_alt)
sol_alt.retcode
ReturnCode.Success = 1

See Algorithm for the full menu of built-in components and Extending for the recipes to define your own.

Observing convergence

The callback API exposes four events during a solve:

hist    = HistoryCallback(; fields = (:k, :F_norm, :α, :n_evals))
alg_cb  = DFProjection(; callbacks = AbstractCallback[hist])
sol_cb  = solve(prob, alg_cb)
length(hist.history), hist.history[end]
(18, (k = 18, F_norm = 1.0443197461587077e-6, α = 1.0, n_evals = 37))

Custom observers subtype AbstractCallback and add a method on on_event!; see Extending §7.

Inspecting termination

using SciMLBase            # for ReturnCode

sol_short = solve(prob, DFProjection(); maxiters = 2, verbose = false)  # we expect (and inspect) a non-Success retcode
sol_short.retcode == ReturnCode.MaxIters
true

The NLStats block on sol.stats carries iteration and evaluation counts.

Lower-level access

Manual step-by-step iteration through the SciML init / step! / solve! contract is available:

prob  = NonlinearProblem(F, x0)
cache = init(prob, DFProjection())
while !cache.inner.done
    step!(cache)                   # one outer iteration
end
sol = solve!(cache)

Here step! is CommonSolve.step!, reexported through SciMLBase. DFMethods does not export an unqualified step! — that would collide with the SciML reexport. The truly internal advance on the inner DFProjectionCache is available qualified: DFMethods.step!(cache.inner).