SimpleSolvers

SimpleSolvers.BACKTRACKING_GROW_MINConstant
const BACKTRACKING_GROW_MIN

Lower bound on the factor by which the expansion phase of Backtracking lengthens an accepted step: unless the model minimiser lies at least $\mathrm{BACKTRACKING\_GROW\_MIN}\cdot\alpha$, the trial step is kept and no further merit evaluation is spent (see backtracking_extrapolation). Its value is 2.0.

This is the counterpart of BACKTRACKING_SHRINK_MIN on the growing side, and it is what makes the expansion phase free for a well-scaled direction: a Newton or BFGS step is already at its model minimum, so the test fails and the search returns at once.

source
SimpleSolvers.BACKTRACKING_SHRINK_MINConstant
const BACKTRACKING_SHRINK_MIN

Lower bound on the factor by which a rejected step is shrunk by the interpolation in Backtracking: the new trial step is confined to $[\mathrm{BACKTRACKING\_SHRINK\_MIN}\cdot\alpha, p\alpha]$ (see [1, §3.5], [2, Alg. A6.3.1]). Its value is 0.1.

source
SimpleSolvers.DEFAULT_ARMIJO_τ_ULPSConstant
const DEFAULT_ARMIJO_τ_ULPS

The nominal number of units in the last place (ulps) of $\varphi(0)$ taken as the round-off resolution $\tau$ of a merit function, i.e. $\tau = \mathrm{DEFAULT\_ARMIJO\_τ\_ULPS}\cdot\mathrm{ulp}(\varphi(0))$ (see armijo_tolerance). Its value is 4.

Use armijo_ulps rather than this constant: it caps the nominal value at what the element type can actually support, which matters in Float16.

$\tau$ is used for three things, and it is worth keeping them apart:

  1. it slackens the SufficientDecreaseCondition inside Backtracking to $\varphi(\alpha) \leq \min\{\varphi(0),\ \varphi(0) + c_1\alpha\varphi'(0) + \tau\}$. The $\min$ is what keeps the allowance honest: it may reduce the decrease demanded, but it can never license a step whose merit exceeds $\varphi(0)$;
  2. it fixes the smallest informative trial step $\alpha_\mathrm{min}$ below which the condition could only be decided by rounding (see backtracking_αmin);
  3. every LinesearchMethod uses it to decide whether an accepted step's decrease was genuine (LINESEARCH_DECREASED) or within the noise (LINESEARCH_FLOOR, see LinesearchOutcome).

Set τ_ulps = 0 in Backtracking to recover the exact condition for (1) and (2).

source
SimpleSolvers.DEFAULT_LINESEARCH_αmaxConstant
const DEFAULT_LINESEARCH_αmax

The largest step length a LinesearchMethod will try unless a caller asks for less; its value is 65536.0 ($2^{16}$). See linesearch_αmax for the two ways the ceiling is set and method_αmax for the per-method field it defaults.

An absolute number is the right shape here, and this is the one place in the package where that is worth arguing. $\alpha$ scales a direction that has already been chosen, so it is a step-length fraction and of order one — the same reason the bracket-width tolerance of BierlaireQuadratic is absolute while its merit comparisons are not. A ceiling of $2^{16}$ is therefore four to five orders of magnitude of headroom above any step a well-scaled direction asks for, and still rules out the $\alpha \approx 4\cdot10^7$ that an unbounded bracketing search can reach on a merit whose minimiser is far away or whose fit is nearly flat.

The value is StrongWolfe's, which has carried exactly this field since before the other methods had one; DEFAULT_WOLFE_αmax is now defined as this constant so the two cannot drift apart.

Use default_linesearch_αmax rather than T(DEFAULT_LINESEARCH_αmax) to obtain it in a given precision: $2^{16}$ is above floatmax(Float16), so the plain conversion overflows to Inf and silently removes the ceiling in the precision the package otherwise takes most care over.

The ceiling is not a decrease criterion

A search stopped by the ceiling has not failed: it returns the largest step it was allowed to take, with the merit measured there, and classifies it by the usual round-off allowance $\tau$. There is no LinesearchOutcome for "capped", because a caller that supplied a ceiling can compare it against the step it got.

source
SimpleSolvers.DEFAULT_WOLFE_c₂Constant
const DEFAULT_WOLFE_c₂

The constant used in the second Wolfe condition (the CurvatureCondition). According to [1, 3] we should have

\[c_2 \in (c_1, 1),\]

where $c_1$ is the constant specified by DEFAULT_WOLFE_c₁.

Furthermore [1] recommend $c_2 = 0.9$; in [3] the authors write: "it is common to set $c_2=0.1$ when approximate line search is used with the conjugate gradient method and to 0.9 when used with Newton's method." We use $c_2 = 0.9$ as default.

source
SimpleSolvers.DEFAULT_WOLFE_αmaxConstant
const DEFAULT_WOLFE_αmax

Default upper bound on the step length for the bracketing phase of StrongWolfe. This is DEFAULT_LINESEARCH_αmax, which every method's ceiling now defaults to: the field was StrongWolfe's alone until the bracketing searches were found to extrapolate without one, and defining this as that constant is what keeps the two from drifting apart. The constructor obtains it through default_linesearch_αmax, which saturates it at floatmax(T) rather than overflowing to Inf in Float16.

source
SimpleSolvers.DOGLEG_Δ_EXPANDConstant

Factor by which the trust-region radius is expanded on a very good step ($\rho > 3/4$ at the boundary); the default of the dogleg_radius_expand field of Options, which the solver actually reads.

source
SimpleSolvers.DOGLEG_Δ_MAXConstant

Default maximum trust-region radius ($\hat\Delta$ in [1, Alg. 4.1]) for the DogLegSolver; the radius is never expanded beyond this. The default of the dogleg_radius_max field of Options, which the solver actually reads.

source
SimpleSolvers.DOGLEG_Δ_SHRINKConstant

Factor by which the trust-region radius is shrunk on a poor step ($\rho < 1/4$); the default of the dogleg_radius_shrink field of Options, which the solver actually reads.

source
SimpleSolvers.F_STALL_FACTORConstant
const F_STALL_FACTOR

The factor by which the residual has to drop for an iteration to count as progress; the default of the f_stall_factor field of Options. Its value is 0.5, i.e. progress means "the residual halved".

record_progress! keeps the residual $r^f_a$ of the last iteration that counted as progress, and counts the iterations since, so that iterations_since_progress measures how long the residual has been going nowhere. That one number is used twice: unconditionally by nonlinear_solver_warnings to explain a solve that spent its whole budget, and — only when f_stall_window > 0 — as the stopping criterion no_progress.

A factor closer to one (say 0.999, i.e. "any improvement of a tenth of a percent counts") makes the measurement more permissive: a residual creeping down that slowly keeps resetting the clock, so only one that is essentially flat is ever reported. A smaller factor demands a steeper descent of an iteration before it counts as progress, and so reports sooner.

Only $0 < f_{\mathrm{stall\,factor}} \le 1$ is meaningful. Nothing asserts it — Options validates none of its fields — but a factor above one would make a residual that grew count as progress and so let the reference climb, which is exactly the monotonicity record_progress! relies on to be immune to a residual that jumps around.

See F_STALL_WINDOW.

source
SimpleSolvers.F_STALL_REPORT_MINIMUMConstant
const F_STALL_REPORT_MINIMUM

The fewest iterations without progress that spent_without_progress will report on. Its value is 10.

Unlike f_stall_window this is not configurable, because it is not a policy: it is the point below which the proportion that predicate measures is not evidence of anything. At two iterations the proportion is satisfied by a single one, and a solve that gets nowhere for four iterations has not shown you anything a solve that got somewhere on the fifth would not.

It is the second of the two guards spent_without_progress carries. The first is isconverged — a residual that stopped improving because it was already small enough is success, not stagnation, and that is what excuses the short solve which converged on its successive-change criterion without halving its residual on the last step. This minimum then covers the solve that has not converged and is merely too short for the proportion to mean anything.

This bounds only what is said about a solve, never what is done with it — the stopping criterion is f_stall_window alone (see no_progress).

source
SimpleSolvers.F_STALL_WINDOWConstant
const F_STALL_WINDOW

The default number of iterations without progress after which a NonlinearSolver gives up; the default of the f_stall_window field of Options. Its value is 0, which disables the criterion (see no_progress).

This is the counterpart of max_stalls for a solve whose iterate keeps moving: the residual sits on a floor far above the requested tolerance while the step is nowhere near the round-off level of $x$, so neither stalled_step nor either convergence branch can fire and the iteration spends max_iterations in full. That floor is typically set by the problem itself — a model, discretisation or ansatz error that $F$ cannot resolve — rather than by round-off, so no $\mathrm{eps}(T)$-scaled tolerance can bound it.

It is off by default because the threshold is a policy, not a test, and a wrong one gives up on a healthy solve. An iteration converging linearly with rate $\rho$ improves by $\rho^W$ over a window $W$: at f_stall_factor = 0.5 and f_stall_window = 50 every $\rho > 2^{-1/50} \approx 0.986$ is abandoned, and a PicardSolver on a stiff problem is slower than that. There is no value that is right for every problem, which is why the diagnosis is unconditional (a solve that spent its budget without progressing says so, see nonlinear_solver_warnings) and only the stopping is opt-in: the report costs a solve that has already failed nothing, whereas the criterion can cost a solve that would have succeeded everything.

So set it once you have seen the report and know the floor is real — a window of a few tens of iterations then turns a full budget into a prompt answer.

source
SimpleSolvers.HAS_PREALLOCATED_GETRFConstant
HAS_PREALLOCATED_GETRF

Whether this Julia's LinearAlgebra.LAPACK.getrf! accepts a pre-allocated pivot vector.

getrf!(A, ipiv) arrived after the 1.10 LTS, and it is the whole reason factorize! can be allocation-free for LapackLU. Where it is missing, the one-argument getrf!(A) is used and its pivot vector copied into the cache, which costs one O(n) allocation per factorization — 3.3 kB at n = 384. The O(n^2) working matrix is reused on every version, and ldiv! is allocation-free on every version, because getrs! has always taken the pivot vector as an argument.

Since LapackLU is the default (see default_linear_solver_method), on the LTS that allocation lands once per factorization in a nonlinear solve. Closing it would mean calling getrf through a hand-written ccall to pass a pre-allocated pivot vector — the only place in the package reaching past the stdlib — for a benefit confined to a Julia version with a finite lifetime. Not worth the maintenance; when the compat floor reaches 1.11 this constant and its else branch delete cleanly.

This is a feature check rather than a VERSION comparison, so the exact release it arrived in does not have to be tracked here.

source
SimpleSolvers.MAX_STALLSConstant
const MAX_STALLS

The default number of consecutive stalled steps after which a NonlinearSolver gives up; the default of the max_stalls field of Options. Its value is 2.

A step is stalled when it does not move the iterate while the residual is not small (see stalled_step), i.e. when the merit $\|F\|^2$ cannot be reduced along the current direction. One stalled step is not conclusive, because the next step is guaranteed to be attempted under better conditions: a stall forces a fresh Jacobian immediately (see maybe_refactorize! and needs_refresh) rather than waiting for the next refactorize multiple, and the DogLegSolver additionally resets a collapsed trust-region radius. A second consecutive stall is therefore one that a freshly evaluated Jacobian did not fix, which is conclusive — and it is conclusive for every refactorize, not just refactorize = 1.

Set max_stalls = typemax(Int) to restore the previous behaviour of running all the way to max_iterations.

source
SimpleSolvers.BacktrackingType
Backtracking <: LinesearchMethod

Keys

The trial step $\alpha$ is not a key: it is the argument of solve, which is its only source. (A Backtracking used to carry an α₀ field for it, which the algorithm never read — see issue #174.) Neither is αmax: this search shrinks, so the trial step is already its ceiling and the expansion phase carries its own in $q^{\mathrm{nexpand}}$. A params.αmax still binds — on the trial step itself as well as on the expansion — since a caller that says no step above a given length is admissible means the first trial too. See SimpleSolvers.linesearch_αmax.

The keys are:

  • c₁=0.0001: the constant $c_1$ in the SufficientDecreaseCondition (Armijo condition). Also see DEFAULT_WOLFE_c₁.
  • c₂=0.9: the constant on whose basis the CurvatureCondition is tested. We should have $c_2\in(c_1, 1).$ The closer this constant is to 1, the easier it is to satisfy the CurvatureCondition.
  • p=0.5: an upper bound on the factor by which $\alpha$ is decreased in every step until the stopping criterion is satisfied. The actual factor is chosen by interpolation and confined to $[$ BACKTRACKING_SHRINK_MIN $\cdot\alpha, p\alpha]$, so the trial sequence is never longer than the plain $\alpha \gets p\alpha$ ladder.
  • expand=false: whether the search may lengthen the trial step (the expansion phase described below). Off by default, so a Backtracking is the classical one-sided algorithm unless it is asked for. Setting it requires the merit to be evaluable — finite or not, but not throwing — out to $q^{\mathrm{nexpand}}\alpha$, since that is the largest step the phase can try.
  • q=10.0: an upper bound on the factor by which $\alpha$ is increased in one expansion round — the counterpart of $p$. Only used when expand is set. See DEFAULT_BACKTRACKING_q.
  • nexpand=3: the cap on the number of expansion trials, each of which costs one merit evaluation. It bounds the phase from within the linesearch_max_iterations of Options rather than beside it: whichever of the two is smaller applies, so the whole search still spends at most linesearch_max_iterations merit evaluations. Only used when expand is set. See DEFAULT_BACKTRACKING_NEXPAND.
  • τ_ulps=armijo_ulps(T, c₁) (4 in Float64 and Float32, less in Float16): the round-off resolution of the merit, in units in the last place of $\varphi(0)$. It slackens the SufficientDecreaseCondition (never past $\varphi(0)$), fixes $\alpha_\mathrm{min}$, and separates a genuine decrease from one within the noise. A value larger than armijo_ulps(T, c₁) is capped to it, since above that $\tau$ would swamp the decrease the condition demands. See DEFAULT_ARMIJO_τ_ULPS.

Implementation

The algorithm starts by setting

\[\begin{aligned} \varphi_0 &\gets \varphi(0),\\ d_0 &\gets \varphi'(0), \end{aligned}\]

where $\varphi$ is of type LinesearchProblem. Unless $\varphi_0$ and $d_0$ are finite with $d_0 < 0$ the search is abandoned at once — no $\alpha$ can satisfy the SufficientDecreaseCondition along a direction that is not decreasing, so shrinking $\alpha$ would only waste merit evaluations to find that out.

Otherwise it sets the round-off resolution $\tau$ (armijo_tolerance) and the smallest informative step $\alpha_\mathrm{min}$ (backtracking_αmin), and shrinks the trial step by backtracking_interpolation until one of the following happens:

  1. the SufficientDecreaseCondition is satisfied — the step is accepted, and reported as a genuine decrease only if $\varphi(\alpha) \leq \varphi_0 - \tau$;
  2. two consecutive trials return $\varphi(\alpha) = \varphi_0$ bit-exactly — the trial point no longer differs from the base point in floating point, so no smaller step can either;
  3. $\alpha \leq \alpha_\mathrm{min}$ — a smaller step could only be judged by rounding;
  4. the linesearch_max_iterations budget of Options is spent.

Cases 2–4 are distinguished in the returned LinesearchStatus: a merit that does not vary by more than $\tau$ has reached its round-off floor (LINESEARCH_FLOOR, benign and not improvable by any line search), whereas one that does vary contradicts $d_0 < 0$ (LINESEARCH_EXHAUSTED, a genuine inconsistency). See LinesearchOutcome.

The CurvatureCondition is not used to terminate the iteration — it cannot be honoured by shrinking alone — it is only checked afterwards to emit a warning (see curvature_diagnostic). With expand set it is satisfied more often, because the expansion phase moves the accepted step toward the line minimiser, so the diagnostic fires less.

The expansion phase

With expand = true and the first trial step accepted, the search may also lengthen it, by backtracking_extrapolation, while each longer trial still satisfies the SufficientDecreaseCondition and strictly improves the merit; at most nexpand such trials are made and the best step seen is returned. A shrunken step is never expanded again: once the ladder has backtracked, the longer steps are already known to fail.

This is the one place where the search leaves the interval $[0, \alpha]$ the caller offered. The largest step it can try is $q^{\mathrm{nexpand}}\alpha$ — a thousand times the trial step on the defaults — and a trial whose merit is not finite is simply rejected, at the cost of the one evaluation. A merit that throws outside its domain is the caller's to guard, and it is one reason the phase is opt-in.

The trials it spends come out of the linesearch_max_iterations budget of Options, not out of a second budget beside it, so termination case 4 above still bounds the whole search: the phase makes at most nexpand trials and at most as many as that budget has left.

This is what makes the search two-sided. A shrink-only search returns the trial step it was given whenever that step is acceptable, so on a direction whose natural scale is larger than the trial step it pins $\alpha$ at that ceiling on every iteration and the outer solve crawls — by two orders of magnitude in the DFP case of issue #174, where the direction wanted $\alpha \approx 11$ throughout.

The phase costs nothing where it can gain nothing. The model it extrapolates from is the same quadratic that backtracking_interpolation uses on the way down, built from $\varphi(0)$, $\varphi'(0)$ and the merit at the trial step — all three already known — so the decision whether to expand at all is free, and a direction that is already scaled like a Newton step (which is at its model minimum at $\alpha = 1$) fails the test and returns without a further merit evaluation. That matters because for the merit of a NonlinearSolver an evaluation is a full residual evaluation, the most expensive single operation of a solver step, which is also why the phase does not test the CurvatureCondition to decide when to stop growing: that would cost a full Jacobian per trial. Use StrongWolfe, which brackets on the derivative, where curvature control is genuinely required.

Extended help

Sometimes the parameters $p$ and $c_1$ have different names such as $\tau$ and $c$. Note that our $\tau$ is something else entirely (the round-off resolution above).

$\alpha_\mathrm{min}$ is a factor $2\,$ τ_ulps above the step $\alpha^*$ at which the condition degenerates into a test decided by rounding — provided backtracking_αmin's upper clamp at $\sqrt{\mathrm{eps}(T)}$ is inactive, which it is for a merit of ordinary steepness in double precision. Where the clamp binds (a very flat merit, or any merit in Float16) the search does trial steps below $\alpha^*$. That is deliberate and harmless: the $\min$ in the SufficientDecreaseCondition means the test there reduces to $\varphi(\alpha) \leq \varphi_0$, i.e. plain monotonicity, and such an accept is reported as LINESEARCH_FLOOR rather than as a decrease.

source
SimpleSolvers.BierlaireQuadraticType
BierlaireQuadratic <: LinesearchMethod

Algorithm taken from [4].

Keywords

  • ε: the bracket-width tolerance of the fit.
  • ξ: the threshold below which $|\varphi'(\alpha_0)|$ counts as stationary.
  • αmax: the largest step the triple-point bracketing will try, by default DEFAULT_LINESEARCH_αmax. Without it the bracketing doubles its increment until the merit stops falling, which for a nearly flat or distantly-minimised $\varphi$ is arbitrarily far; see linesearch_αmax, which is also how a caller imposes a smaller ceiling of its own.
source
SimpleSolvers.BisectionType
Bisection <: LinesearchMethod

See bisection for the implementation of the algorithm.

Keywords

Extended help

When invoked with a single trial step α (i.e. solve(ls, α)), the bracket is always lower-anchored at $\alpha = 0$ — the only point where a genuine descent direction is guaranteed to have a decreasing merit ($\varphi'(0) < 0$), which one-sided rightward bracketing requires. The caller's α is then folded in via one extra derivative evaluation (see issue #164):

  • if $\varphi'(\alpha) \geq 0$ then α overshot the minimum and $[0, \alpha]$ already brackets a stationary point, so it is handed straight to bisection with no bracketing loop;
  • otherwise $\alpha$ still lies on the descent side, so the bracket is grown outward from $0$ with the initial step seeded from $|\alpha|$ — clamped between DEFAULT_BRACKETING_s and 1 so a large α does not over-coarsen the search and a tiny α does not crawl — rather than the fixed default step.

This keeps the safe $\alpha = 0$ anchor while letting the caller's α set the search scale and, when it overshoots, serve directly as the upper bracket bound.

What it converges to is a minimum, not a maximum

Bisection drives on the sign of $\varphi'$, so it converges to whichever crossing the sign at its left endpoint selects — and that sign is invariant under the halving. From $\varphi'(\mathrm{lo}) < 0$ the interval shrinks onto a $-\to+$ crossing, a minimum; from $\varphi'(\mathrm{lo}) > 0$ onto a $+\to-$ crossing, a maximum.

Nothing in bracket_minimum rules the second out. It brackets a minimum in value, sampling $\varphi$ and never $\varphi'$, so on a non-convex ray its interval can enclose several stationary points and its left endpoint can sit past one of them. Landing on a maximum used not to be caught: the step was classified by the merit like any other step, so it was LINESEARCH_DECREASED when it happened to improve $\varphi$ and LINESEARCH_FLOOR when it did not — the same overclaim the previous section describes, reached by a different route, and GeometricOptimizers observed exactly it (LINESEARCH_FLOOR reported with $\varphi(1) = \varphi(0)$, and the step taken regardless).

The orientation is now checked and repaired: when $\varphi'(\mathrm{lo}) > 0$ the search bisects $[0, \mathrm{lo}]$ instead, which brackets a minimum by construction — the anchor check has established $\varphi'(0) < 0$ — and finds the earlier one, nearer the anchor. The check costs nothing on a ray that does not need it, because it reads a value the bisection computes anyway. See SimpleSolvers._bisect_for_minimum.

A bracket that fails is never a floor

Bisection drives on the sign of $\varphi'$, so it can only work on an interval whose endpoints straddle a sign change. When bracket_minimum hands it an interval that brackets a minimum in value but over which $\varphi'$ keeps its sign — a non-smooth or noisy merit, or a derivative inconsistent with it — there is nothing to bisect and _bisection_core reports BISECTION_NOBRACKET.

That case used to be folded into "converged", so the endpoint with the smallest $|\varphi'|$ was claimed as the line minimiser and, when it did not improve the merit, classified as LINESEARCH_FLOOR — which asserts that no line search can make progress along this direction and makes the outer iteration count the step towards max_stalls. A failed bracket establishes nothing of the kind. So the outcome is now classified by the merit alone: LINESEARCH_DECREASED when the returned step still beats $\varphi(0)$ by more than $\tau$, and LINESEARCH_EXHAUSTED when it does not. LINESEARCH_FLOOR is reachable only from a bisection that actually converged.

source
SimpleSolvers.BisectionOutcomeType
BisectionOutcome

Why _bisection_core stopped. This is what lets a caller tell "found the root" from "gave up" — a distinction a Bool cannot carry, and whose absence made an unbracketable derivative look like a located line minimiser (see Bisection).

  • BISECTION_CONVERGED: a root was located, either because $|f(\alpha)| \leq$ f_abstol or because the bracket collapsed to x_suctol.
  • BISECTION_NOBRACKET: the endpoint values share a sign, so the interval contains no root of odd multiplicity and bisection cannot start. The endpoint with the smallest $|f|$ is returned, but it is not a root — this is a failure to report, not a result.
  • BISECTION_EXHAUSTED: the linesearch_max_iterations budget of Options was spent with the interval still straddling a sign change. Unlike BISECTION_NOBRACKET there is a root in the interval, so a larger budget would find it; the best estimate so far is returned.

This is internal: it is the return type of a private function, like the Symbol that _triple_point_core reports. Callers of a line search see a LinesearchOutcome instead.

source
SimpleSolvers.CurvatureConditionType
CurvatureCondition <: BacktrackingCondition

The second of the Wolfe conditions [1]. The first one is the SufficientDecreaseCondition.

This encompasses the standard curvature condition and the strong curvature condition. This can be specified via the mode keyword.

With the standard curvature condition we check:

\[f'(\alpha) ≥ c_2 d,\]

where $c_2$ is the associated hyperparameter and $d$ is the derivative at $\alpha_0$. Further note that $f'(\alpha_0)$ and $d$ should both be negative.

With the strong curvature condition we check:

\[|f'(\alpha)| ≤ c_2 |d|.\]

Constructor

CurvatureCondition(c, d₀, D, Val(:Standard))
CurvatureCondition(c, d₀, D, Val(:Strong))

Here D has to be a function computing the derivative of the objective. The mode is passed as a Val (defaulting to Val(:Standard)) so that it is encoded in the type and dispatch — and hence inference — is stable without relying on constant propagation of a Symbol keyword. The other inputs are numbers.

source
SimpleSolvers.DogLegType
DogLeg(refactorize=1)

Powell's dogleg method [5].

Like Newton, the refactorize parameter determines after how many steps the Jacobian is re-evaluated and refactored (see factorize!). The default refactorize = 1 re-evaluates and refactorizes the Jacobian on every step; refactorize > 1 reuses the Jacobian (and its factorization) in between, giving a quasi-Newton-style dogleg method.

source
SimpleSolvers.DogLegSolverMethod
DogLegSolver(x, nlp::NonlinearProblem, y = zero(x))

Build a DogLegSolver for the NonlinearProblem nlp with the initial guess x. See NewtonSolver(::AbstractVector{T}, ::NonlinearProblem, ::AbstractVector{T}) where {T} for the rôle of y; as above, no linesearch keyword is accepted — the step length comes from the trust-region radius.

Keywords

source
SimpleSolvers.GradientAutodiffType
GradientAutodiff <: Gradient

A struct that realizes Gradient by using ForwardDiff.

Keys

The struct stores:

  • F: a function that has to be differentiated.
  • ∇config: result of applying ForwardDiff.GradientConfig.

Constructors

GradientAutodiff(F, x::AbstractVector)
GradientAutodiff{T}(F, nx::Integer)

Functor

The functor does:

grad(g, x) = ForwardDiff.gradient!(g, grad.F, x, grad.∇config)
source
SimpleSolvers.GradientFiniteDifferencesType
GradientFiniteDifferences <: Gradient

A struct that realizes Gradient by using finite differences.

Keys

The struct stores:

  • F: a function that has to be differentiated.
  • ϵ: small constant on whose basis the finite differences are computed.
  • e: auxiliary vector used for computing finite differences. It's of the form $e_1 = \begin{bmatrix} 1 & 0 & \cdots & 0 \end{bmatrix}^T$.
  • tx: auxiliary vector used for computing finite differences. It stores the offset in the x vector.

Constructor(s)

GradientFiniteDifferences{T}(F, nx::Integer; ϵ)

By default for ϵ is default_ϵ(T).

Functor

The functor does (for grad(g, x)):

for j in eachindex(x,g)
    ϵⱼ = grad.ϵ * abs(x[j]) + grad.ϵ
    fill!(grad.e, 0)
    grad.e[j] = 1
    grad.tx .= x .- ϵⱼ .* grad.e
    f1 = grad.F(grad.tx)
    grad.tx .= x .+ ϵⱼ .* grad.e
    f2 = grad.F(grad.tx)
    g[j] = (f2 - f1) / (2ϵⱼ)
end
source
SimpleSolvers.GradientFunctionType
GradientFunction <: Gradient

A struct that realizes a Gradient by explicitly supplying a function.

Keys

The struct stores:

  • F: a function that has to be differentiated.
  • ∇F!: a function that can be applied in place.

Functor

The functor does:

grad(g, x) = grad.∇F!(g, x)
source
SimpleSolvers.HessianType
Hessian

Abstract type. structs derived from this need an associated functor that computes the Hessian of a function (in-place).

Also see Gradient.

Implementation

When a custom Hessian is implemented, a functor is needed:

function (hessian::Hessian)(h::AbstractMatrix, x::AbstractVector) end

Examples

Examples include:

source
SimpleSolvers.HessianAutodiffType
HessianAutodiff <: Hessian

A struct that realizes Hessian by using ForwardDiff.

Keys

The struct stores:

  • F: a function that has to be differentiated.
  • Hconfig: result of applying ForwardDiff.HessianConfig.

Constructors

HessianAutodiff{T}(F, Hconfig)
HessianAutodiff(F, x::AbstractVector)
HessianAutodiff{T}(F, nx::Integer)

Functor

The functor does:

hes(H, x) = ForwardDiff.hessian!(H, hes.F, x, hes.Hconfig)
source
SimpleSolvers.HessianFunctionType
HessianFunction <: Hessian

A struct that realizes a Hessian by explicitly supplying a function.

Keys

The struct stores:

  • H!: a function that can be applied in place.

Functor

The functor does:

hes(H, x) = hes.H!(H, x)
source
SimpleSolvers.JacobianMethod
Jacobian{T}(F, nx, ny; mode = :autodiff, kwargs...)

Construct a Jacobian of element type T for a function F mapping nx inputs to ny outputs, selecting the backend via the mode keyword:

The convenience forms Jacobian{T}(F, n), Jacobian(F, x) and Jacobian(F, x, y) forward here.

source
SimpleSolvers.JacobianAutodiffType
JacobianAutodiff <: Jacobian

A struct that realizes Jacobian by using ForwardDiff.

Keys

The struct stores:

  • F: a function that has to be differentiated.
  • Jconfig: result of applying ForwardDiff.JacobianConfig.
  • ty: vector that is used for evaluating ForwardDiff.jacobian!

Constructors

JacobianAutodiff(F, x::AbstractVector)
JacobianAutodiff{T}(F, nx::Integer)

Functor

The functor does:

jac(J, x, params) = ForwardDiff.jacobian!(J, (y, x) -> jac.F(y, x, params), jac.ty, x, jac.Jconfig)
source
SimpleSolvers.JacobianFiniteDifferencesType
JacobianFiniteDifferences <: Jacobian

A struct that realizes Jacobian by using finite differences.

Keys

The struct stores:

  • F: a function that has to be differentiated.
  • ϵ: small constant on whose basis the finite differences are computed.
  • f1: $f$ evaluated at $x - \epsilon_j e_j$ with $\epsilon_j = \epsilon|x_j| + \epsilon$ for all $j$.
  • f2: $f$ evaluated at $x + \epsilon_j e_j$ with $\epsilon_j = \epsilon|x_j| + \epsilon$ for all $j$.
  • e: auxiliary vector used for computing finite differences. It's of the form $e_1 = \begin{bmatrix} 1 & 0 & \cdots & 0 \end{bmatrix}^T$.
  • tx: auxiliary vector used for computing finite differences. It stores the offset in the x vector.

Constructor(s)

JacobianFiniteDifferences{T}(F, nx::Integer, ny::Integer; ϵ)

By default for ϵ is default_ϵ(T).

Functor

The functor does:

for j in eachindex(x)
    ϵⱼ = jac.ϵ * abs(x[j]) + jac.ϵ
    fill!(jac.e, 0)
    jac.e[j] = 1
    jac.tx .= x .- ϵⱼ .* jac.e
    jac.F(jac.f1, jac.tx, params)
    jac.tx .= x .+ ϵⱼ .* jac.e
    jac.F(jac.f2, jac.tx, params)
    for i in eachindex(jac.f1)
        J[i,j] = (jac.f2[i] - jac.f1[i]) / (2ϵⱼ)
    end
end
source
SimpleSolvers.JacobianFunctionType
JacobianFunction <: Jacobian

A struct that realizes a Jacobian by explicitly supplying a function taken from the NonlinearProblem.

Functor


f(y, x, params) = y .= [1. √2.; √2. 3.] * x
∇f(j, x, params) = j .= [1. √2.; √2. 3.]

jac = JacobianFunction(f, ∇f, Float64)
j = zeros(Float64, 2, 2)
x = ones(Float64, 2)

jac(j, x, NullParameters())

# output

2×2 Matrix{Float64}:
 1.0      1.41421
 1.41421  3.0
source
SimpleSolvers.LUType
struct LU <: DirectMethod

A custom implementation of an LU solver, meant to solve a LinearProblem.

Routines that use the LU solver include factorize!, ldiv! and solve!.

Constructor

The constructor is called with either no argument:

LU()

# output

LU{Missing}(missing, true)

or with pivot and static as optional booleans:

LU(; pivot=true, static=true)

# output

LU{Bool}(true, true)

Note that if we do not supply an explicit keyword static, the corresponding field is missing (as in the first case). In that default case the cache matrix type is chosen by size via _static: a matrix whose leading dimension does not exceed N_STATIC_THRESHOLD yields a mutable static (MMatrix) cache, a larger one yields a plain Matrix. An explicit static=true/false forces the choice regardless of the matrix size.

Example

We use the LU together with solve to solve a linear system:

A = [1. 2. 3.; 5. 7. 11.; 13. 17. 19.]
v = rand(3)
ls = LinearProblem(A, v)

lu = LU()

solve(lu, ls) ≈ inv(A) * v

# output

true

Note that role of LinearProblem here.

source
SimpleSolvers.LUSolverCacheType
LUSolverCache <: LinearSolverCache

The cache for the LU solver.

Keys

  • A: the factorized matrix A,
  • pivots: a vector of pivots used during factorization,
  • perms: a vector of permutations used during factorization,
  • info: stores an index regarding pivoting.
source
SimpleSolvers.LapackLUType
struct LapackLU <: PivotedLUMethod

A LAPACK-backed LU solver, meant to solve a LinearProblem.

Where LU is a self-contained implementation that works for any number type and for static matrices, this method delegates the factorization to LAPACK's getrf. It is restricted to the element types LAPACK handles — Float32, Float64, ComplexF32 and ComplexF64 — and throws an ArgumentError naming the type when handed anything else. Any AbstractMatrix storage is accepted, but the cache always holds a plain Matrix, because that is what LAPACK can be pointed at.

Routines that use it are the same as for LU: factorize!, ldiv! and solve!, all shared with the other PivotedLUMethods through PivotedLUCache.

Constructor

LapackLU()

# output

LapackLU()

When to use which

LU is the better choice for very small systems, where its static-matrix cache avoids allocation altogether, and it is the only choice for element types LAPACK does not know about. LapackLU is the better choice everywhere else, and is the default for a dense matrix of a LAPACK element type — see default_linear_solver_method. Measured on an Apple M4 Max against OpenBLAS, in microseconds for factorize! including the copy-in:

nLU(static=false)LapackLURecursiveLU
120.240.630.14
6422.910.86.65
12818259.642.5
3846526531961
7685110916137349

LapackLU's triangular solve is the faster one too, by 3.5–4.5× across that whole range: 21.9 µs against LU's 77.3 µs at n = 384. See RecursiveLU for where the crossover in the factorization sits and why it depends on the BLAS in use.

The effect on a real problem is large. Measured from PoissonBrackets.jl, where a Newton step factorizes a dense $384 \times 384$ Jacobian, LU accounted for 74 % of the cost of one implicit time step — about 17 ms against 0.6 ms for the same factorization through LAPACK.

Allocation is not part of the trade-off. Like LU, and unlike a bare LinearAlgebra.lu!, both factorize! and ldiv! are allocation-free after the LinearSolver is built: the working matrix and the pivot vector are allocated once and reused. See PivotedLUCache.

The one exception is factorize! on a Julia too old for LAPACK.getrf!(A, ipiv) — the 1.10 LTS — where the pivot vector costs one O(n) allocation per call. The O(n^2) working matrix is reused on every version, and ldiv! is allocation-free on every version. See HAS_PREALLOCATED_GETRF.

Example

A = [1. 2. 3.; 5. 7. 11.; 13. 17. 19.]
v = rand(3)
ls = LinearProblem(A, v)

solve(LapackLU(), ls) ≈ inv(A) * v

# output

true
source
SimpleSolvers.LinearProblemType
LinearProblem

A LinearProblem describes $Ax = y$, where we want to solve for $x$.

Keys

  • A
  • y

Constructors

A LinearProblem can be allocated by calling:

LinearProblem(A, y)
LinearProblem(A)
LinearProblem(y)
LinearProblem{T}(n, m)
LinearProblem{T}(n)

LinearProblem(A, y) stores copies of A and y, so the problem is ready to solve right after construction (and later mutations of the caller's arrays do not affect the stored copies):

A = [1. 2. 3.; 4. 5. 6.; 7. 8. 9.]
y = [1., 2., 3.]
ls = LinearProblem(A, y)

# output

LinearProblem{Float64, Vector{Float64}, Matrix{Float64}}([1.0 2.0 3.0; 4.0 5.0 6.0; 7.0 8.0 9.0], [1.0, 2.0, 3.0])

The size-only constructors (LinearProblem(A), LinearProblem(y), LinearProblem{T}(n[, m])) allocate the unspecified parts as NaNs; use update! to fill the system with values:

ls = LinearProblem(y)
update!(ls, A, y)

# output

LinearProblem{Float64, Vector{Float64}, Matrix{Float64}}([1.0 2.0 3.0; 4.0 5.0 6.0; 7.0 8.0 9.0], [1.0, 2.0, 3.0])
source
SimpleSolvers.LinearSolverType
LinearSolver <: AbstractSolver

A struct that stores LinearSolverMethods (for example LU) and LinearSolverCaches (for example LUSolverCache). LinearSolvers are used to solve LinearProblems.

Constructors

LinearSolver(method, cache)
LinearSolver(method, A)
LinearSolver(method, ls::LinearProblem)
LinearSolver(method, x)
Info

We note that the constructors do not call the function factorize, so only allocate a new matrix. The factorization needs to be done manually.

You can manually factorize by either calling factorize! or solve!.

source
SimpleSolvers.LinearSolverMethodType
LinearSolverMethod <: SolverMethod

Summarizes all the methods used for solving linear systems of equations such as the LU method.

Extended help

The abstract type SolverMethod was imported from GeometricBase.

source
SimpleSolvers.LinesearchMethodType
LinesearchMethod{T} <: SolverMethod

Examples include Static, Backtracking, Bisection , BierlaireQuadratic and Quadratic. See these examples for specific information on linesearch algorithms.

Extended help

A LinesearchMethod is usually used in Linesearch (or with solve).

It is a subtype of SolverMethod (imported from GeometricBase) — line searches are one-dimensional subproblems used inside nonlinear solvers and optimizers, so (unlike a NonlinearSolverMethod) a LinesearchMethod is not itself a nonlinear-solver method.

The line search contract

Every method reached through solve or solve_with_status guarantees:

  1. It never throws. A situation it cannot handle is reported, never raised — a line search must not abort the enclosing solve. Bracketing helpers signal failure with nothing (see bracket_minimum, triple_point_finder) and the method maps that onto a LinesearchOutcome. The one deliberate exception is a caller error rather than a situation arising from the merit: a params.αmax that is not a usable ceiling raises an ArgumentError before any evaluation — see linesearch_αmax.
  2. It returns $\alpha > 0$. Never the $\alpha = 0$ anchor, which would freeze the outer iterate (x .+= 0 .* d), and never a negative step: $\alpha$ scales a direction that has already been chosen, so its sign is not the line search's to decide.
  3. It reports through linesearch_warnings only — one message site and one verbosity policy for all methods (genuine failure at verbosity ≥ 1; the benign round-off-floor and stationary outcomes at ≥ 2). And it reports there only when the user called it: a program calls solve_with_status, acts on the LinesearchStatus and sees no messages at all. A NonlinearSolver is such a program — see record_linesearch!. This is structural rather than a convention a method has to keep: a method implements solve_with_status, and solve is derived from it once, for all methods, as that call plus the report.
  4. A non-finite or ascending anchor is reported, not assumed away — see check_anchor.
  5. It terminates in a bounded number of merit evaluations, independently of the merit's scale. Multiplying $\varphi$ by a constant must not change the cost.
  6. It returns $\alpha \leq \alpha_\mathrm{max}$ — see linesearch_αmax for the two ways that ceiling is set and why one of them has to be per call.

The two families

What is not standardised is the meaning of the input $\alpha$ and what each method guarantees about the step, because there are two distinct kinds:

  • Condition-satisfying, $\alpha$-relativeBacktracking, StrongWolfe and trivially Static: "given the trial step $\alpha$, return a step satisfying a decrease condition". The result depends on the input $\alpha$. Backtracking shrinks it only, and therefore returns it unchanged whenever it is acceptable, unless its expand key is set; StrongWolfe brackets in both directions.
  • Minimising, $\alpha$-independentBisection, Quadratic and BierlaireQuadratic: "approximate the minimiser of $\varphi$ along the direction". The input $\alpha$ only seeds the bracketing (see issue #164), and no Wolfe condition is checked.
source
SimpleSolvers.LinesearchOutcomeType
LinesearchOutcome

Why a LinesearchMethod stopped. Stored in a LinesearchStatus, which is returned by solve_with_status.

  • LINESEARCH_DECREASED: a step was found that decreased the merit by more than the round-off allowance $\tau$. This is the only outcome that reports progress. Note what it does not claim: Backtracking and StrongWolfe additionally verify their Wolfe condition before returning, whereas the minimising searches (Bisection, Quadratic, BierlaireQuadratic) approximate the line minimiser and never test one. The common guarantee across all of them is the $\tau$-exceeding decrease.
  • LINESEARCH_FLOOR: the merit has reached its round-off floor — no trial step changes it by more than $\tau$, so no line search can make progress here. The returned step is the smallest informative one. This is not an error and is only reported at verbosity ≥ 2: it is the expected final state of a converged solve, and when it is not — when the residual is still large — the outer iteration reports it as stagnation instead (see stalled_step and Options).
  • LINESEARCH_EXHAUSTED: no acceptable step although the merit does vary by more than $\tau$. Either $\varphi'(0)$ is inconsistent with $\varphi$ (a stale or regularized Jacobian, an inexact linear solve, a non-smooth merit), or the linesearch_max_iterations budget of Options was spent.
  • LINESEARCH_NO_DESCENT: $\varphi'(0) > 0$, or $\varphi(0)$/$\varphi'(0)$ is not finite. No $\alpha$ can satisfy the sufficient decrease condition.
  • LINESEARCH_STATIONARY: $\varphi'(0) = 0$, e.g. a vanishing direction at an exact root. Benign — there is nothing to search for.
  • LINESEARCH_UNKNOWN: the method does not report an outcome — Static, which evaluates no merit and so has established nothing, and any third-party method that chooses not to report one.
source
SimpleSolvers.LinesearchProblemType
LinesearchProblem <: AbstractProblem

In practice LinesearchProblems are allocated by calling linesearch_problem.

Constructors

Calling line search problems

Below we show a constructor that can be used to allocate a LinesearchProblem. Note however that in practice one should call linesearch_problem and not use the constructor directly.

f(x) = x^2 - 1
g(x) = 2x
δx(x) = - g(x) / 2
x₀ = 3.
_f(α,_) = f(compute_new_iterate(x₀, α, δx(x₀)))
_d(α,_) = g(compute_new_iterate(x₀, α, δx(x₀)))
ls_obj = LinesearchProblem{typeof(x₀)}(_f, _d)

# output

LinesearchProblem{Float64, typeof(_f), typeof(_d)}(_f, _d)
source
SimpleSolvers.LinesearchStatusType
LinesearchStatus{T}

The step length returned by a line search together with the diagnostics needed to tell progress from stagnation. Obtained from solve_with_status; compare this to NonlinearSolverStatus, which plays the same role for the outer iteration.

The step length alone cannot express the difference: a tiny $\alpha$ may be the correct answer, or it may be all that is left after the merit turned out to be irreducible. See LinesearchOutcome.

Keys

  • α: the returned step length (the value solve returns),
  • outcome::LinesearchOutcome,
  • trials: what the search cost, as the number of times it evaluated the problem — not the linesearch_max_iterations budget. That is the merit for every method except Bisection, which drives on the derivative it bisects and brackets on the merit, so its count is of both. For Backtracking and StrongWolfe it is exactly the number of trial steps $\alpha > 0$: every merit evaluation is either the $\alpha = 0$ anchor or a counted trial. For the searches that bracket it includes what the bracketing spent (bracket_minimum, triple_point_finder) — that is where those searches do their work, and on the path where a ceiling binds it is the whole of it, so a count omitting it reported one evaluation, or none at all, for a search of any size. One of the evaluations it then counts is the bracketing's own re-evaluation of the anchor, so for those methods the number is the cost rather than exactly the number of distinct positive steps,
  • φ₀, d₀: the merit and its derivative at the anchor $\alpha = 0$,
  • φ: the merit at the returned step,
  • τ: the round-off resolution of the merit (see armijo_tolerance), against which every method decides whether the decrease it achieved was genuine,
  • αmin: the smallest step length that could still be decided by the merit rather than by rounding (see backtracking_αmin). This is a shrinking-ladder quantity and is therefore zero — meaning "not applicable" — for the minimising searches (Bisection, Quadratic, BierlaireQuadratic) and for StrongWolfe, which bracket rather than shrink.
source
SimpleSolvers.NewtonType
Newton(refactorize=1)

The Newton (and quasi-Newton) nonlinear solver method.

Constructors

Newton()

# output

Newton(1)
QuasiNewton()

# output

Newton(5)
Info

The refactorize parameter determines how often the Jacobian is re-evaluated and refactored (see factorize!). The default refactorize = 1 refactorizes on every step (a plain Newton method), whereas refactorize > 1 reuses the factorization in between, giving a quasi-Newton method (conveniently constructed via QuasiNewton).

source
SimpleSolvers.NewtonSolverType
NewtonSolver

A const derived from NonlinearSolver as NewtonSolver{T} = NonlinearSolver{T,Newton}.

Constructors

The NewtonSolver can be called with a NonlinearProblem or with a Callable.

See NewtonSolver(::AbstractVector{T}, ::NonlinearProblem, ::AbstractVector{T}) where {T} and NewtonSolver(::AbstractVector{T}, ::Callable, ::AbstractVector{T}) where {T}.

F(y, x, params) = y .= sin.(x) ^ 2
x = ones(5)
y = zeros(5)

ns = NewtonSolver(x, F, y)
typeof(ns) <: NewtonSolver

# output

true

Keywords

  • linear_solver_method: the method used to build the linear solver (see LinearSolver) that computes the direction of the solver step (see solver_step!),
  • DF!: an in-place function computing the Jacobian,
  • linesearch::Linesearch
  • jacobian::Jacobian
  • jacobian_prototype: the matrix whose storage and, if sparse, whose sparsity pattern are adopted by the Jacobian, the LinearProblem and the LinearSolver cache. It is copied, so the caller's matrix is left alone. A SparseMatrixCSC here is what runs a sparse Jacobian through the solver, and it requires DF! (see checkjacobianprototype) and a pattern that includes the diagonal if regularization_factor is non-zero. It also selects the default linear_solver_method; see default_linear_solver_method,
  • refactorize::Int: determines after how many steps the Jacobian is re-evaluated and refactored (see factorize!). refactorize > 1 gives a quasi-Newton method (see QuasiNewton),
  • options_kwargs: see Options
source
SimpleSolvers.NewtonSolverMethod
NewtonSolver(x, nlp::NonlinearProblem, y = zero(x))

Build a NewtonSolver for the NonlinearProblem nlp with the initial guess x, assembling the Jacobian, the LinearProblem, the LinearSolver, the Linesearch and the NonlinearSolverCache.

y is a prototype for the residual $F(x)$: it supplies a size and an element type, and nothing that is computed from it survives (alloc_j turns it into a NaN matrix and the cache stores zero(y)). It is not, however, left alone — JacobianAutodiff keeps it as the buffer ForwardDiff writes the residual into, so a caller-supplied y is overwritten on every Jacobian evaluation. It defaults to zero(x), which is what a square system needs — and every system here is square, since the LinearSolver factorizes the Jacobian.

Info

The default is zero(x) rather than similar(x) because alloc_j broadcasts over y: for an element type whose similar leaves undefined references (BigFloat, say) an uninitialized prototype throws an UndefRefError. It also assumes zero(x) has the same type as x, which NonlinearSolverCache requires; for an x where it does not — a SubArray, whose zero is an Array — pass y explicitly.

The Jacobian stored in nlp (if any) takes precedence over autodiff, exactly as the DF! keyword of NewtonSolver(::AbstractVector{T}, ::Callable, ::AbstractVector{T}) where {T} does — see resolve_jacobian.

Keywords

  • linear_solver_method
  • linesearch
  • jacobian
  • jacobian_prototype
  • refactorize
  • options_kwargs: see Options

Examples

F(y, x, params) = y .= sin.(x) .^ 2
x = ones(3)
nlp = NonlinearProblem(F, x)

NewtonSolver(x, nlp) isa NewtonSolver

# output

true
source
SimpleSolvers.NonlinearProblemType
NonlinearProblem

A NonlinearProblem describes $F(x) = y$, where we want to solve for $x$ and $F$ is in nonlinear in general (also compare this to LinearProblem).

Keys

  • F
  • J::Union{Callable, Missing}: accessed by calling jacobian.

Constructors

We show an example for one particular constructor:

F(y, x, params) = y .= sin.(x) .^ 2
NonlinearProblem(F, zeros(3))

# output

NonlinearProblem{typeof(F), Missing}(F, missing)
source
SimpleSolvers.NonlinearSolverType
NonlinearSolver

A struct that comprises Newton solvers (see Newton), the Picard solver (also known as fixed-point iteration; see Picard) and the Dogleg solver (see DogLeg).

Info

The associated solvers are consts derived from NonlinearSolver. See NewtonSolver, PicardSolver and DogLegSolver. In practice we usually call those associated constructors directly rather than creating a NonlinearSolver instance manually.

Keys

source
SimpleSolvers.NonlinearSolverCacheType
NonlinearSolverCache <: AbstractNonlinearSolverCache

Derived from AbstractNonlinearSolverCache. Used in NonlinearSolver.

Keys

  • x: the next iterate (or guess thereof),
  • Δx: search direction. This is updated when calling solver_step! via the LinearSolver stored in the NewtonSolver,
  • rhs: the right-hand-side (this can be accessed by calling rhs),
  • y: the problem evaluated at x,
  • j::AbstractMatrix: the Jacobian evaluated at x. Note that this is not of type Jacobian!
Info

The line search reads the current search direction Δx from this cache but writes its trial iterate, residual and Jacobian into its own private buffers (see linesearch_problem); it does not overwrite x, y or j.

source
SimpleSolvers.NonlinearSolverMethodType
NonlinearSolverMethod <: SolverMethod

A supertype collecting all nonlinear solver methods, i.e. Newton, Picard and DogLeg.

Compare this with LinesearchMethod: both are subtypes of SolverMethod, but a LinesearchMethod describes a one-dimensional line search (used inside a solver step) whereas a NonlinearSolverMethod describes the outer nonlinear iteration itself.

source
SimpleSolvers.NonlinearSolverStatusType
NonlinearSolverStatus

Stores absolute and successive residuals for x and f. It is used as a diagnostic tool in NewtonSolver.

Info

Compare this to the NonlinearSolverState and the NonlinearSolverCache.

Keys

The line-search tally is the programmatic channel

A line search does not log from inside a solve — it reports to the solver, which accumulates the outcomes here (see record_linesearch!). A caller that wants to act on a rejected line search rather than read about it — restart an approximate Hessian, fall back to steepest descent — reads this tally instead of scraping the log. That is what solve_with_status! is for.

Examples

x = [1., 2., 3., 4.]
state = NonlinearSolverState(x)
cache = NonlinearSolverCache(x, x)
config = Options()
NonlinearSolverStatus(state, config)

# output

i=   0,
rxₛ= NaN,
rfₐ= NaN,
rfₛ= NaN
source
SimpleSolvers.OptionsType
Options

Examples

Options()

# output

                x_abstol = 4.440892098500626e-16
                x_reltol = 4.440892098500626e-16
                x_suctol = 4.440892098500626e-16
                f_abstol = 0.0
                f_reltol = 1.4901161193847656e-8
                f_suctol = 4.440892098500626e-16
                f_mindec = 0.0001
          f_stall_factor = 0.5
          f_abstol_break = Inf
       allow_f_increases = true
          min_iterations = 0
          max_iterations = 1000
         warn_iterations = 1000
linesearch_max_iterations = 60
              max_stalls = 2
          f_stall_window = 0
              show_trace = false
             store_trace = false
          extended_trace = false
              show_every = 1
               verbosity = 1
      nan_max_iterations = 10
              nan_factor = 0.5
   regularization_factor = 0.0
   dogleg_radius_initial = 1.0
    dogleg_radius_shrink = 0.25
    dogleg_radius_expand = 2.0
       dogleg_radius_max = 100.0
Info

The tolerance constants (x_abstol through f_suctol) default to values derived from default_tolerance and absolute_tolerance, except f_reltol, which defaults to √eps(T): it is the relative residual tolerance used by assess_convergence — the residual is small when rfₐ ≤ f_abstol + f_reltol·‖F(x₀)‖, i.e. the absolute tolerance is f_abstol and the relative tolerance is f_reltol.

Info

dogleg_radius_initial, dogleg_radius_shrink, dogleg_radius_expand and dogleg_radius_max are the trust-region parameters for the DogLegSolver: the initial and maximum radius ($\Delta_0$ and $\hat\Delta$ in [1, Alg. 4.1]) and the factors by which the radius is shrunk on a poor step / expanded on a very good boundary step. They default to DOGLEG_Δ_INITIAL, DOGLEG_Δ_SHRINK, DOGLEG_Δ_EXPAND and DOGLEG_Δ_MAX, and are ignored by the other solvers.

`max_iterations` versus `linesearch_max_iterations`

max_iterations bounds the outer nonlinear iteration (see meets_stopping_criteria); linesearch_max_iterations bounds the inner, one-dimensional line search taken within a single solver step — the Backtracking ladder, the StrongWolfe bracketing and zoom phases, bisection, and the Quadratic/BierlaireQuadratic fits. These used to be the same field, which meant that capping the solver at max_iterations = 50 silently also capped the ladder, and that the default of 1000 was applied to a ladder which can never need more than $\lceil-\log_2\varepsilon\rceil$ trials. See linesearch_iterations.

Choosing `f_abstol`

f_abstol is an absolute target for $\|F(x)\|$, and the default 0 (see absolute_tolerance) is never met by a nonzero residual: the absolute branch of assess_convergence is switched off by default and convergence is decided entirely by the relative (f_reltol) and successive (x_suctol, f_suctol) branches.

Conversely, an f_abstol below the round-off floor of your own residual — the cancellation level of the terms F sums internally, which the solver cannot see — is unsatisfiable. The iteration then reaches that floor, stops making progress, and is reported as stagnated (see max_stalls, stalled_step and nonlinear_solver_warnings) rather than converged.

Note that f_reltol does not rescue this case: the relative gate is anchored at the initial residual $\|F(x_0)\|$, so an excellent initial guess makes it tighter, not looser. If the stagnation warning reports an achieved rfₐ near your f_abstol, raise f_abstol above it — an order of magnitude of headroom is usual.

A residual floor need not come from round-off at all. A model, discretisation or ansatz error — an approximation F is built on and cannot resolve — puts a floor on $\|F(x)\|$ that may sit many orders of magnitude above the round-off level, and that no $\mathrm{eps}(T)$-scaled tolerance can bound. Such a solve looks different from a stalled one: the iterate keeps moving normally, so nothing stops the iteration and it spends max_iterations in full. It is reported by nonlinear_solver_warnings as having made no progress, and f_stall_window (see F_STALL_WINDOW) bounds its cost once you know the floor is there. The remedy is the same, one level up: raise f_abstol above the achieved rfₐ, or improve the approximation until its floor lies below the tolerance you need.

source
SimpleSolvers.PicardSolverMethod
PicardSolver(x, F)

Arguments

  • x: the initial guess for the solution.
  • F: the nonlinear function to solve.
  • y

Keywords

Note that the Picard solver_step! is a residual-safeguarded fixed-point iteration and uses no line search, so — unlike the other solvers — no linesearch keyword is accepted (passing one is an error rather than being silently ignored).

Examples

F(y, x, params) = y .= sin.(x) .^ 2
x = zeros(2)
y = similar(x)

s = PicardSolver(x, F, y)
state = SolverState(s)

solve!(x, s, state)

# output

2-element Vector{Float64}:
 0.0
 0.0
source
SimpleSolvers.PivotedLUCacheType
PivotedLUCache <: LinearSolverCache

The cache shared by every PivotedLUMethodLapackLU and RecursiveLU.

Keys

  • A: the working copy of the matrix, which the factorization overwrites in place,
  • ipiv: the pivot vector the factorization fills,
  • info: the index of the first zero pivot, or 0 on success,
  • factorized: whether factorize! has run at all, which info == 0 cannot express.

The cache holds the pieces of the factorization rather than a LinearAlgebra.LU object, so that factorize! can be called any number of times without allocating: LU is immutable, so a fresh one would have to be built and boxed into a field on every refactorization, and its ipiv freshly allocated. Both are O(n)-to-O(1) costs against an O(n^3) factorization, but a nonlinear solve in a time-stepping loop refactorizes on every step of every step, and LU — the method these sit beside — allocates nothing at all. (On a Julia without LAPACK.getrf!(A, ipiv) — the 1.10 LTS — LapackLU fills ipiv from a per-call temporary instead, which costs one O(n) allocation per factorization; see HAS_PREALLOCATED_GETRF.)

Use factorization to get a LinearAlgebra.LU view of these pieces when one is actually wanted (for det, say).

The pivot vector is typed Vector{LinearAlgebra.BlasInt} rather than Vector{Int}: that is what getrf fills, and the two are not the same type under a 32-bit-integer BLAS. It is also what RecursiveLU writes into, whose ipiv is any AbstractVector{<:Integer} — so one cache type serves both, and the LAPACK triangular solve can be shared with it.

source
SimpleSolvers.PivotedLUMethodType
PivotedLUMethod <: DirectMethod

The methods that compute a partially-pivoted LU factorization in LAPACK's layout, and can therefore share a cache, a triangular solve and everything built on them: LapackLU and RecursiveLU.

They differ only in which kernel computes the factors — see _getrf! — and in which element types they accept. See PivotedLUCache.

source
SimpleSolvers.QuadraticType
Quadratic <: LinesearchMethod

Quadratic Polynomial line search based on the polynomial

\[p(α) = p_0 + p_1(\alpha - \alpha_0) + p_2(\alpha - \alpha_0)^2.\]

Performs multiple iterations in which all parameters $p_0$, $p_1$ and $p_2$ are adapted. We do not check the SufficientDecreaseCondition here. We instead repeatedly build new quadratic polynomials until a minimum is found (to sufficient accuracy). The iteration may also stop after it reaches the maximum number of iterations, the linesearch_max_iterations field of Options (see linesearch_iterations).

Keywords

  • ε: A constant that checks the precision/tolerance.
  • s: A constant that determines the initial interval for bracketing. By default this is DEFAULT_BRACKETING_s.
  • s_reduction: A constant that determines the factor by which s is decreased in each new bracketing iteration.
  • αmax: the largest step the bracketing will try, by default DEFAULT_LINESEARCH_αmax. Without it the bracket grows outward until the merit stops falling, which for a nearly flat or distantly-minimised $\varphi$ is arbitrarily far; see linesearch_αmax, which is also how a caller imposes a smaller ceiling of its own.

Extended help

The quadratic method. Compare this to BierlaireQuadratic. The algorithm is adjusted from [6].

source
SimpleSolvers.RecursiveLUType
struct RecursiveLU <: PivotedLUMethod

A RecursiveFactorization.jl-backed LU solver, meant to solve a LinearProblem.

Only available once RecursiveFactorization is loaded; the constructor exists either way, and building a LinearSolver with it says what to load if it is not. It is a package extension because that dependency is heavy — LoopVectorization, Polyester, StrideArraysCore, TriangularSolve and VectorizedRNG — and the win is confined to a middle range of sizes.

Restricted to Float32 and Float64, and to Matrix storage. RecursiveFactorization has no complex support, so unlike LapackLU this covers two of the four BLAS element types; the cache constructor says so by name.

Everything except the factorization itself is shared with LapackLU — the cache, the triangular solve, singular_index and every solve! form — because RecursiveFactorization writes LAPACK-layout factors with LAPACK's pivot convention, so getrs applies unchanged. See PivotedLUMethod and PivotedLUCache.

Like LapackLU, factorize! and ldiv! are allocation-free once the LinearSolver exists, on every Julia version — the pivot vector is pre-allocated in the cache and RecursiveFactorization takes it as an argument.

Constructor

RecursiveLU()

When to use it

For a middle range of sizes, where a blocked pure-Julia kernel beats the BLAS's own but the O(n^3) term has not yet taken over. Measured on an Apple M4 Max, factorize! in microseconds including the copy-in:

nLU(static=false)LapackLU (OpenBLAS)RecursiveLU
120.240.630.14
323.473.361.40
6422.910.86.65
12818259.642.5
2561912169287
3846526531961
7685110916137349
The crossover depends on the BLAS, not just on `n`

Against OpenBLAS — the default, and what most callers will have — RecursiveLU wins for roughly 10 < n ≲ 200. Against a faster getrf the window shrinks sharply: with AppleAccelerate loaded on the same machine, LapackLU factorizes a 384 × 384 in 285 µs rather than 531, and a 128 × 128 in 26.5 µs rather than 59.6 — so it wins from about n = 64 upward. Measure on the machine that matters before choosing this.

Below n ≈ 10 the default LU() uses a static-matrix cache and allocates nothing, which is the better trade there. Above the crossover use LapackLU. For element types neither LAPACK nor RecursiveFactorization handles, LU remains the only option.

No threading option, deliberately

RecursiveFactorization's threaded path is not exposed. Measured under julia -t12 on Julia 1.13 it reproduced every sequential timing up to n = 384 — so it does not parallelize at these sizes — and then stalled at n = 512, with the worker threads parked in a condition wait inside LoopVectorization, on a factorization that takes 2.5 ms sequentially. Val(false) is hardcoded.

source
SimpleSolvers.SparseFactorizationCacheType
SparseFactorizationCache <: LinearSolverCache

The cache shared by the SparseDirectMethods, UmfpackLU and SparspakLU.

Keys

  • F: the backend's factorization object, which owns the ordering, the symbolic factorization and the numeric factors,
  • n: the leading dimension, kept here because the two backends store it differently,
  • info: 0 if the last factorization or solve found nothing wrong, non-zero otherwise; see singular_index,
  • factorized: whether factorize! has run at all.

Unlike PivotedLUCache there is no working copy of the matrix: both backends take the SparseMatrixCSC as an argument to their refactorize call and read its nzval directly, so a copy here would be dead weight. The consequence is that the pattern is fixed at construction — which is exactly the contract a Newton loop wants, since reusing the ordering and symbolic factorization is where the saving is.

Neither backend is allocation-free, and that is inherent to them rather than to this wrapper. Measured on a periodic banded matrix at n = 384: UmfpackLU allocates ~374 kB per refactorization but 0 B per ldiv!; SparspakLU allocates ~11 kB per refactorization and ~10 kB per ldiv!.

source
SimpleSolvers.SparspakLUType
struct SparspakLU <: SparseDirectMethod

A sparse LU solver backed by Sparspak.jl, meant to solve a sparse LinearProblem.

Only available once Sparspak is loaded; the constructor exists either way, and building a LinearSolver with it says what to load if it is not.

Constructor

SparspakLU()

Why this exists alongside UmfpackLU

Element types. Sparspak is generic in the element type where UMFPACK is not. Probed on a periodic banded matrix:

element typeSparspakLUresidualUmfpackLU
Float64works1.1e-16works
Float32works1.2e-7unsupported
ComplexF64works1.1e-16works
BigFloatworks1.7e-77unsupported
Rational{BigInt}works0.0 — exactunsupported

That last row is the point: a sparse solve over ℚ with no rounding at all, which nothing else here can do. It is also a pure-Julia stack, with no SuiteSparse binary. The Float32 row is not a typo either: UMFPACK converts a 32-bit matrix in lu/lu! but has no 32-bit solve, so UmfpackLU refuses those element types at construction rather than failing later inside ldiv!, and this is one of the two methods that cover them.

An exact solve goes through `factorize!` and `ldiv!`

The allocating convenience forms — solve, and the solve!(lsolver, args...) that returns a fresh vector — fill their solution with NaNs, which Rational and Integer element types cannot represent, so they raise for exactly the types this method exists to serve. Build the LinearSolver, call factorize!, and pass your own solution vector to LinearAlgebra.ldiv!. This is package-wide SimpleSolvers._nan policy rather than anything specific to SparspakLU — dense solve(LU(), A, b) raises the same way for a Rational system.

Not for speed on Float64. Its factorization is in fact the faster of the two, by 1.3–1.5×, but its triangular solve is about 9× slower, and a Newton loop does at least one solve per factorization. Measured at n = 384 on a periodic banded matrix: 59.5 µs factorize + 32.7 µs solve against UMFPACK's 76.2 + 3.5. Prefer UmfpackLU for the element types it handles.

Allocation

Neither factorize! nor ldiv! is allocation-free — about 11 kB and 10 kB respectively at n = 384 — and that is inside Sparspak rather than in this wrapper.

Singularity: reported late

Warning

Sparspak has no zero-pivot index and no status field: a singular matrix factorizes without complaint, and the solve then returns non-finite numbers. This wrapper closes that hole by checking the solution in ldiv! and raising SingularException itself — an O(n) check against an O(nnz · fill) factorization, so affordable — but two consequences cannot be papered over:

  • singular_index returns 0 until a solve has actually failed, and is a flag rather than a pivot position even then.
  • DogLegSolver reads singular_index before solving, to decide whether the Newton leg is available (see SimpleSolvers.directions!). With this method that check cannot fire, so a singular Jacobian surfaces as a SingularException out of the solve rather than as a fallback to the steepest-descent leg. Use UmfpackLU with DogLeg where the element type allows it.
source
SimpleSolvers.StaticType
Static <: LinesearchMethod

The static method.

Keys

Keys include:

  • α: equivalent to a step size. The default is 1.

Examples

Static()

# output

Static with α = 1.0.
The caller's ceiling still binds

Static has no αmax field — the whole point of the method is that α is the caller's to fix — but a params.αmax clamps the step it hands back, since a caller that says no step above a given length is admissible means this one too. See SimpleSolvers.linesearch_αmax.

source
SimpleSolvers.StrongWolfeType
StrongWolfe{T} <: LinesearchMethod

A line search that finds a step $\alpha$ satisfying the strong Wolfe conditions

\[\begin{aligned} f(\alpha) &\leq f(0) + c_1\,\alpha\,f'(0), &\text{(sufficient decrease / Armijo)}\\ |f'(\alpha)| &\leq c_2\,|f'(0)|, &\text{(strong curvature)} \end{aligned}\]

with $0 < c_1 < c_2 < 1$. It implements the bracketing line search of [1, Alg. 3.5 and 3.6 (zoom)]: a bracketing phase grows the step until an interval containing an acceptable point is found, then a zoom phase shrinks that interval (by bisection) until the strong Wolfe conditions hold.

Unlike Backtracking — which enforces only sufficient decrease, since the curvature condition cannot be honoured by shrinking alone — StrongWolfe actually enforces the curvature condition, at the cost of evaluating the derivative at each trial step. Use it when curvature control is genuinely required; Backtracking is cheaper otherwise.

Keys

Info

The strong Wolfe conditions require a descent direction ($f'(0) < 0$). If the line search problem is not decreasing at $\alpha = 0$ the method cannot make progress; it then returns the caller's initial step and (at verbosity ≥ 1) warns.

source
SimpleSolvers.SufficientDecreaseConditionType
SufficientDecreaseCondition <: BacktrackingCondition

The condition that determines if the change induced by $\alpha_k$ is big enough. This is used in Backtracking.

Example

c = SimpleSolvers.DEFAULT_WOLFE_c₁
f(x) = (x - 1.) ^ 2
xₖ = 0.
fₖ = f(xₖ)
dₖ = 2xₖ - 2.

sdc = SufficientDecreaseCondition(c, fₖ, dₖ, f)
sdc(1.9), sdc(2.)

# output

(true, false)

Extended help

We call the constant that pertains to the sufficient decrease condition $c$. This is typically called $c_1$ in the literature [1]. See DEFAULT_WOLFE_c₁ for the relevant constant

The optional keyword τ slackens the condition by an absolute amount:

\[f(\alpha) \leq \min\{f_0,\ f_0 + c\alpha{}d_0 + \tau\}.\]

It defaults to zero, i.e. the exact condition. Without it the accept/reject decision is taken by rounding alone as soon as $c\alpha|d_0|$ drops below one unit in the last place of $f_0$: the right-hand side then rounds back up to $f_0$ and the test degenerates to $f(\alpha) \leq f_0$, which a merit that has reached its round-off floor passes or fails at random. See armijo_tolerance and backtracking_αmin for how Backtracking chooses $\tau$ and derives a meaningful smallest step from it.

The $\min$ bounds the slackening: $\tau$ may reduce the decrease that is demanded, but it never accepts a step whose merit exceeds $f_0$. For $d_0 < 0$ — which both callers guarantee via check_anchor — the $\min$ is inactive wherever $f_0 + c\alpha{}d_0$ is representably below $f_0$, so it changes nothing in double precision; it matters at low precision, where $\tau$ can exceed the demanded $c\alpha|d_0|$ outright.

source
SimpleSolvers.UmfpackLUType
struct UmfpackLU <: SparseDirectMethod

A sparse LU solver backed by SuiteSparse's UMFPACK, meant to solve a sparse LinearProblem.

No extension and no new dependency: UMFPACK ships inside the SparseArrays standard library, which this package depends on for the sparse-Jacobian plumbing anyway. Restricted to the element types UMFPACK provides — Float64 and ComplexF64 — and it names the alternatives for anything else. That includes Float32 and ComplexF32: SuiteSparse converts them in lu and lu! but has no 32-bit solve, so a 32-bit factorization would be a ldiv! MethodError waiting to happen rather than a working narrow path. Use SparspakLU to keep a 32-bit matrix sparse, or LapackLU to densify it.

This is the method to reach for when the Jacobian is genuinely sparse and its element type is a standard one. It is also what default_linear_solver_method selects in that case.

Constructor

UmfpackLU()

Why sparse is worth it, and when it is not

Measured on an Apple M4 Max, periodic banded matrices of bandwidth 2, factorize! and ldiv! in microseconds against a dense LapackLU on the same matrix:

nnnzUmfpackLU factorizeldiv!dense LapackLU factorize
6432013.00.6811.3
12864026.31.2859.5
384192076.23.52525
102451202078.62525
40962048096139.8

So the two are a wash around n = 64 and sparse wins by roughly 7× at n = 384 and 12× at n = 1024. A dense matrix handed to this method is an error rather than a conversion — see checksparse — because a SparseMatrixCSC with no structural zeros factorizes slower than LapackLU does.

Against SparspakLU

Sparspak's factorization is the faster of the two, by about 1.3–1.5×. Its triangular solve is about 9× slower, which more than reverses that in a Newton loop, where one factorization is followed by one or more solves. UmfpackLU is the better default; SparspakLU is for the element types UMFPACK cannot do at all.

Allocation

ldiv! is allocation-free. factorize! is not — lu! allocates about 374 kB at n = 384 inside SuiteSparse — and unlike the dense methods that cannot be fixed from here. See SparseFactorizationCache.

Singularity

UMFPACK reports singularity as a status, not as a pivot index, so singular_index is a flag: 0 on success, non-zero otherwise. factorize! records it and ldiv! raises SingularException, matching LapackLU's "report it when it is used" contract so that a quasi-Newton method that factorizes speculatively is not interrupted.

It can be silently wrong on block-structured systems

A sparse direct solver relaxes pivoting to preserve sparsity, and UMFPACK's ordering plus threshold pivoting is not always up to a matrix whose blocks have very different norms — a saddle-point or mixed formulation, for instance. Measured on the 2N × 2N Newton matrix of PoissonBrackets.jl's mixed two-field formulation, whose four banded blocks span seven orders of magnitude: from n = 1536 upward the computed solution is wrong by a factor of 150, with a linear residual 2000× the right-hand side, while issuccess returns true and no exception is raised. Dense LapackLU on the same matrix is accurate to 1e-12, and so is SparspakLU, whose different ordering and pivoting handle it.

This is not general ill-conditioning: on synthetic banded matrices UMFPACK is accurate to 1e-14 at condition numbers of 1e7, well past where these failures start.

So: for a block-structured Jacobian, check the residual of a solve before trusting it, and prefer SparspakLU if it does not hold up. UmfpackLU remains the right default for the banded and mesh-like patterns a discretisation usually produces, where it is both faster and accurate.

source
GeometricBase.update!Method
update!(state, x, y)

Update , , x and y.

Examples

julia> f(y, x, params) = y .= sin.(x .- .5) .^ 2
f (generic function with 1 method)

julia> x = ones(1) / 4
1-element Vector{Float64}:
 0.25

julia> y = zero(x); f(y, x, NullParameters())
1-element Vector{Float64}:
 0.06120871905481365

julia> state = NonlinearSolverState(x)
NonlinearSolverState{Float64, Vector{Float64}, Vector{Float64}}(0, [NaN], [NaN], [NaN], [NaN], NaN, 0, false, NaN, 0, [0, 0, 0, 0, 0, 0])

julia> update!(state, x, y)
NonlinearSolverState{Float64, Vector{Float64}, Vector{Float64}}(0, [0.25], [NaN], [0.06120871905481365], [NaN], NaN, 0, false, NaN, 0, [0, 0, 0, 0, 0, 0])

julia> x = ones(1) / 2
1-element Vector{Float64}:
 0.5

julia> f(y, x, NullParameters())
1-element Vector{Float64}:
 0.0

julia> update!(state, x, y)
NonlinearSolverState{Float64, Vector{Float64}, Vector{Float64}}(0, [0.5], [0.25], [0.0], [0.06120871905481365], NaN, 0, false, NaN, 0, [0, 0, 0, 0, 0, 0])

The NonlinearSolverState stores the previous solution, the previous value, the current solution and the current value.

All of these are updated during one update! step (and initialized with NaNs).

source
LinearAlgebra.ldiv!Method
ldiv!(x, lsolver, b)

Compute inv(cache(lsolver).A) * b by utilizing the factorization of the lu solver (see LU and LinearSolver) and store the result in x.

Examples

julia> A = [1.; 0.; 0.;; 0.; 2.; 0.;; 0.; 0.; 4.]
3×3 Matrix{Float64}:
 1.0  0.0  0.0
 0.0  2.0  0.0
 0.0  0.0  4.0

julia> b = [1., 1., 1.]
3-element Vector{Float64}:
 1.0
 1.0
 1.0

julia> s = LinearSolver(LU(), A); factorize!(s); x = zeros(3)
3-element Vector{Float64}:
 0.0
 0.0
 0.0

julia> ldiv!(x, s, b)
3-element Vector{Float64}:
 1.0
 0.5
 0.25
Info

Note that we need to call factorize! here after having allocated the LinearSolver.

source
SimpleSolvers._bisect_for_minimumMethod
_bisect_for_minimum(ls, lo, hi, params)

Bisect $\varphi'$ on [lo, hi] for a minimum, returning (α, outcome, n) as _bisection_core does without its ylo.

A bisection converges to whichever crossing the sign of $\varphi'$ at its left endpoint selects, and that sign is invariant under the halving (see _bisection_core): from $\varphi'(lo) < 0$ the interval shrinks onto a $- \to +$ crossing, which is a minimum, and from $\varphi'(lo) > 0$ onto a $+ \to -$ crossing, which is a maximum. Nothing in bracket_minimum rules the second out — it brackets a minimum in value, sampling $\varphi$ and never $\varphi'$, so on a non-convex ray its interval can enclose several stationary points and its left endpoint can sit past one of them.

So when $\varphi'(lo) > 0$ this bisects [0, lo] instead, which brackets a minimum by construction: check_anchor has established $\varphi'(0) < 0$, and $\varphi'(lo) > 0$ gives the opposite sign at the other end, so the crossing between them is a $- \to +$ one. It is also the earlier minimum, the one nearer the anchor, which is the one a line search wants.

The repair costs nothing on the path that does not need it: ylo is a value _bisection_core computes anyway, so a correctly oriented bracket is recognised without a single extra evaluation of $\varphi'$ — which for the $\|F\|^2$ merit of a NonlinearSolver is a full Jacobian. Only the pathological ray pays for a second bisection.

The same condition covers the case where [lo, hi] has no crossing at all and ascends at lo (BISECTION_NOBRACKET with ylo > 0): there too a minimum lies in $(0, lo)$ and there too the bisection of [0, lo] finds it. The retry's verdict then replaces the first one rather than being merged with it, unlike the negative-step retry in solve_with_status — the two disagree here because the first bracket was in the wrong place, which is precisely what ylo detected, and not because $\varphi'$ is inconsistent with $\varphi$.

Private; solve_with_status is the public entry point.

source
SimpleSolvers._bracket_coreMethod
_bracket_core(f, x, bc, s, k, nmax, αmax)

The loop of bracket, returning (lo, hi, n, status) with n the number of evaluations of f it spent and status one of :ok, :capped or :unbracketable. Splitting the loop from the reporting is what bisection/_bisection_core and triple_point_finder/_triple_point_core already do, and here it is what lets a caller tell a bracket that ends at the ceiling αmax from one that satisfied the criterion: the first says the turning point lies beyond the largest step the caller allows, so the answer is αmax itself, and the interval is not worth fitting anything to.

n is reported for the same reason the status is: it is the cost of the bracketing, and for the searches that bracket it is most of the cost of the whole line search. Without it the trials of their LinesearchStatus could only ever be a lower bound — vacuous on the path where the bracketing is the search, which is exactly the path a ceiling produces.

Private: bracket and bracket_minimum are the public entry points.

source
SimpleSolvers._bracket_minimum_with_fixed_point_coreMethod
_bracket_minimum_with_fixed_point_core(f, x, s, k, nmax, αmax)

bracket_minimum_with_fixed_point with the number of evaluations of f it spent and the bracket's status appended — :ok, :capped or :unbracketable, as _bracket_core reports them. Private; the split exists so that Quadratic can tell a bracket that ends at the ceiling from one that found a turning point, and hand back the ceiling instead of fitting a polynomial to an interval over which the merit only falls.

source
SimpleSolvers._pivoted_lu_cacheMethod
_pivoted_lu_cache(A)

Build a PivotedLUCache from A.

Any AbstractMatrix storage is accepted, but the cache always holds a plain Matrix, because that is what both backends can be pointed at. It is a copy rather than undef so that the single-argument factorize! has something to factorize — as it does for LU, whose cache is likewise seeded from A.

source
SimpleSolvers._solve_core!Method
_solve_core!(x, s, state, params)

Run the nonlinear iteration and return the NonlinearSolverStatus it ends in, having already reported it through nonlinear_solver_warnings and print_status.

This is the shared body of solve!, which returns x, and of solve_with_status!, which returns the status. It exists so that the status is built once: the loop has to build one anyway to report on itself, and having solve_with_status! ask status for a second one afterwards spent five l2norm passes per solve reproducing a value it had just discarded — on exactly the path whose reason for existing is to keep per-solve work out of a caller's loop.

The two readings still cannot disagree, and for a stronger reason than before: nothing touches the state between the end of the loop and the caller, so a later status(s, state) rebuilds the same value from the same fields. That is asserted in the test suite.

Private: solve! and solve_with_status! are the public entry points.

source
SimpleSolvers._sparse_ldiv!Function
_sparse_ldiv!(method, cache, x, b)

Solve with the factorization in cache, writing the result to x, and report a singular factorization as a SingularException.

The per-method half of ldiv! for a SparseDirectMethod; the shared half does the guards. The two backends differ in when they can tell that the matrix was singular, which is why this is not shared. See singular_index.

source
SimpleSolvers._staticMethod
_static(A)

Determine whether the LUSolverCache for a default LU should store A as a mutable static matrix (MMatrix) or as a plain Matrix. Every matrix whose leading dimension is smaller than or equal to N_STATIC_THRESHOLD is stored as an MMatrix.

This is only consulted for the default LU() (i.e. LU{Missing}); an explicit static=true/false keyword overrides it. See the examples in factorize!.

Size is not the only condition: an MArray cannot setindex! a non-isbitstype element, so a BigFloat matrix gets a plain Matrix cache at every size. Without that, the default LU() — which default_linear_solver_method selects for exactly those element types — built an MMatrix cache for anything up to N_STATIC_THRESHOLD and then failed inside factorize! with StaticArrays' "setindex!() with non-isbitstype eltype is not supported", a long way from the choice that caused it.

source
SimpleSolvers.absolute_toleranceMethod
absolute_tolerance(T)

Determine the absolute tolerance for a specific data type. This is used in the constructor of Options.

In comparison to default_tolerance, this should return a very small number, close to zero (i.e. not just machine precision).

Examples

julia> absolute_tolerance(Float64)
0.0
julia> absolute_tolerance(Float32)
0.0f0
source
SimpleSolvers.add_to_diagonal!Method
add_to_diagonal!(A, α)

Add α to every diagonal entry of A, in place.

Returns immediately for α == 0, which is the default (SimpleSolvers.REGULARIZATION_FACTOR), so the regularization step of SimpleSolvers.maybe_refactorize! costs nothing unless it was asked for.

For a sparse matrix the diagonal has to be structurally present; a linear-index view into a SparseMatrixCSC, as the dense path uses, would both be wrong for a structural zero and cost O(log nnz) per entry. A missing diagonal entry is an error, because a regularized Jacobian needs somewhere to put the shift.

source
SimpleSolvers.alloc_rhsMethod
alloc_rhs(A)

Allocate a dense right-hand-side/solution vector of length size(A, 1) for the matrix A.

Deliberately dense even when A is not. The obvious spelling, alloc_x(A[:, 1]), gives a sparse vector for a SparseMatrixCSC, and NaN * 0 != 0, so the sparse broadcast has to store every entry anyway — a sparse vector with no structural zeros, which is strictly worse than the Vector it should have been. The right-hand side of a linear system is dense in every caller here.

source
SimpleSolvers.armijo_ulpsMethod
armijo_ulps(T, c₁)
armijo_ulps(T)

The number of ulps of $\varphi(0)$ to use as the round-off resolution $\tau$ for element type T: the nominal DEFAULT_ARMIJO_τ_ULPS, capped at what T can support. The one-argument form uses DEFAULT_WOLFE_c₁.

$\tau$ has to satisfy two requirements that pull in opposite directions. To recognise a merit sitting at its round-off floor it must be at least an ulp or so of $\varphi(0)$. To leave the SufficientDecreaseCondition meaningful it must be far below the decrease that condition demands, which for the canonical $\|F\|^2$ merit of a Newton step ($\varphi'(0) = -2\varphi(0)$) is $2c_1\varphi(0)$ at $\alpha = 1$. Hence the cap

\[n \leq \frac{\mathtt{ARMIJO\_τ\_DEMAND\_FRACTION}\cdot 2c_1}{\mathrm{eps}(T)} .\]

The two requirements are compatible only while $\mathrm{eps}(T) \ll 2c_1$. They are, by a wide margin, in double precision (the cap is $\sim10^{10}$) and comfortably in single ($\sim17$), so the nominal 4 stands in both. They are not in Float16, where $\mathrm{eps}(T) = 9.8 \cdot 10^{-4}$ already exceeds $2c_1 = 2\cdot10^{-4}$: no value of τ_ulps above zero can satisfy both, so the cap resolves the conflict in favour of a meaningful condition and drops $\tau$ to about $2\cdot10^{-3}$ ulps — in effect the exact condition.

Nothing is lost by that. The floor is still detected, by two mechanisms that do not depend on $\tau$: at a trial step small enough that $\mathrm{fl}(\varphi(0) + c_1\alpha\varphi'(0))$ rounds back to $\varphi(0)$ the condition is $\varphi(\alpha) \leq \varphi(0)$, and Backtracking additionally stops on two consecutive bit-identical merits. What is gained is that a genuine decrease of one or two ulps — the smallest a Float16 merit can express — is reported as LINESEARCH_DECREASED instead of as LINESEARCH_FLOOR, which the outer iteration would otherwise count towards max_stalls.

Examples

julia> armijo_ulps(Float64), armijo_ulps(Float32)
(4.0, 4.0f0)
julia> armijo_ulps(Float16)
Float16(0.002075)
source
SimpleSolvers.assess_convergenceMethod
assess_convergence(rxₛ, rfₐ, rfₛ, config, state)

Assess convergence for status::NonlinearSolverStatus and return the triple (x_converged, f_converged, f_increased).

The successive-change criteria (in x and f) alone are not sufficient to declare convergence: a stalled step (e.g. an artificially tiny line-search step) makes the successive residuals rxₛ and rfₛ vanish even when the absolute residual rfₐ is large. We therefore require the residual to be small — written residual_small below — in addition to the successive-change criterion before reporting convergence. The residual passes the standard atol + rtol·‖F₀‖ test:

residual_smallrfₐ ≤ config.f_abstol + config.f_reltol * initial_residual(state),

with the absolute tolerance atol = config.f_abstol (defaulting to 0) and the relative tolerance rtol = config.f_reltol (defaulting to √eps(T)) applied to the initial residual ‖F(x₀)‖. Concretely:

  • x_converged: rxₛ ≤ norm(solution(state)) * config.x_suctol and residual_small,
  • f_converged: (rfₛ ≤ norm(value(state)) * config.f_suctol and residual_small) or rfₐ ≤ config.f_abstol,
  • f_increased: norm(value(state)) > norm(previousvalue(state)).

This guards the successive-change criteria against stagnation: it is loose enough that a genuinely converged iterate satisfies it (the successive-change criteria still supply the tight, machine-precision accuracy) yet tight enough to reject a step that stalls near its initial residual (rfₐ ≈ ‖F(x₀)‖ ≫ f_reltol·‖F(x₀)‖). The relative term is what lets a well-scaled solve whose residual floors at a large absolute value (e.g. a large-magnitude or ill-conditioned F) still converge; it drops to zero (leaving the pure absolute f_abstol test) until the state has been initialized (initial_residual is NaN).

Also see meets_stopping_criteria.

source
SimpleSolvers.backtracking_extrapolationMethod
backtracking_extrapolation(φ₀, d₀, α, φα, q)

The next trial step of the expansion phase of Backtracking, or α itself to say that the step should not be lengthened.

α/φα is the step that was just accepted. The model is the same quadratic through $\varphi(0)$, $\varphi'(0)$ and $\varphi(\alpha)$ that backtracking_interpolation uses on the first backtrack, and its minimiser is

\[\alpha^\star = \frac{-\varphi'(0)\,\alpha^2}{2\big(\varphi(\alpha) - \varphi(0) - \varphi'(0)\alpha\big)} ,\]

clamped from above to $q\alpha$. A denominator that is negative or zero means the model is not convex — the merit fell at least as fast as its tangent, so it is still dropping steeply — and the step grows by the full factor $q$. A denominator that is not finite is a different thing entirely, namely no model at all, and returns α.

Everything the model needs has already been evaluated, so the decision costs no merit evaluation. That is what the lower bound BACKTRACKING_GROW_MIN is for: unless the step the search would actually try is at least that multiple of $\alpha$, α is returned unchanged and the search stops without spending one. The test is on the clamped step rather than on $\alpha^\star$ itself, which matters only for $q <$ BACKTRACKING_GROW_MIN: there the clamp, not the model, is what decides, and a convex model must not be allowed to buy a growth by $q$ that a non-convex one is refused. A direction scaled like a Newton step has $\alpha^\star \approx \alpha$ at $\alpha = 1$ and therefore pays nothing at all, and a merit sitting at its round-off floor ($\varphi(\alpha) \approx \varphi(0)$) gives $\alpha^\star \approx \alpha/2$, so the model declines to expand into rounding noise without needing a special case for it.

source
SimpleSolvers.backtracking_interpolationMethod
backtracking_interpolation(φ₀, d₀, α, φα, αp, φp, p)

The next trial step of the safeguarded polynomial backtracking used by Backtracking (see [1, §3.5], [2, Alg. A6.3.1]).

α/φα is the trial step that was just rejected and αp/φp the one rejected before it (αp is NaN on the first backtrack). The model interpolates $\varphi(0)$, $\varphi'(0)$ and the rejected value(s) — a quadratic on the first backtrack, a cubic afterwards — and its minimiser is clamped to $[$ BACKTRACKING_SHRINK_MIN $\cdot\alpha, p\alpha]$.

The clamp is what makes this safe: an unclamped interpolant can return $\alpha$ itself (no progress at all), collapse to numerically zero, or be meaningless because the merit values it is built from are rounding noise. Because the upper bound is $p$, the trial sequence is pointwise never longer than the plain $\alpha \gets p\alpha$ ladder.

source
SimpleSolvers.backtracking_αminMethod
backtracking_αmin(c₁, d₀, τ)

The smallest step length for which the SufficientDecreaseCondition can still be decided by the merit rather than by rounding:

\[\alpha_\mathrm{min} = \frac{\tau}{c_1|\varphi'(0)|} .\]

Below $\alpha_\mathrm{min}$ the demanded decrease $c_1\alpha|\varphi'(0)|$ is smaller than the round-off resolution $\tau$, so a trial step carries no information. Writing $\tau = n\cdot\mathrm{ulp}(\varphi(0))$ (see armijo_tolerance) gives

\[\alpha_\mathrm{min} = 2n\,\alpha^*, \qquad \alpha^* = \frac{\mathrm{ulp}(\varphi(0))}{2c_1|\varphi'(0)|},\]

where $\alpha^*$ is the step below which $\mathrm{fl}(\varphi(0) + c_1\alpha\varphi'(0))$ rounds back up to $\varphi(0)$ and the condition degenerates to $\varphi(\alpha) \leq \varphi(0)$.

The result is clamped to $[\mathrm{eps}(T), \sqrt{\mathrm{eps}(T)}]$: the lower bound is the historical negligible-step floor, and the upper bound makes sure that a nearly flat but genuine merit (very small $|\varphi'(0)|$) is still searched — an unclamped $\alpha_\mathrm{min}$ grows without bound as $|\varphi'(0)| \to 0$ and would stop the search before it began.

The factor ``2n`` holds only while the upper clamp is inactive

$\alpha_\mathrm{min} = 2n\,\alpha^*$ puts the search a factor $2n$ clear of the region where the condition is decided by rounding, but the $\sqrt{\mathrm{eps}(T)}$ clamp can pull it below $\alpha^*$: that happens for $|\varphi'(0)| < \mathrm{ulp}(\varphi(0)) / (2c_1\sqrt{\mathrm{eps}(T)})$, i.e. below $7\cdot10^{-5}$ for Float64 with $\varphi(0) = 1$, below $1.7$ for Float32, and essentially always for Float16. Trial steps below $\alpha^*$ are then taken, and that is safe rather than merely tolerated: the $\min$ in the SufficientDecreaseCondition reduces the test there to $\varphi(\alpha) \leq \varphi(0)$, so it can accept a non-increase but never an increase, and such an accept is classified LINESEARCH_FLOOR.

source
SimpleSolvers.bisectionMethod
bisection(f, αmin, αmax, params, config)

Perform bisection of f in the interval [αmin, αmax] with Options config.

The algorithm is repeated until a root is found (up to tolerance config.f_abstol which is determined by default_tolerance by default).

Info

When calling bisection it first checks if $x_\mathrm{min} < x_\mathrm{max}$ and else flips the two entries.

Info

You can also call bisection with only one x as input argument. It then uses bracket_minimum to find a suitable interval.

Extended help

The bisection algorithm divides an interval into equal halves until a root is found (up to a desired accuracy).

We first initialize:

\[\begin{aligned} \alpha_0 \gets & \alpha_\mathrm{min}, \\ \alpha_1 \gets & \alpha_\mathrm{max}, \end{aligned}\]

and then repeat:

\[\begin{aligned} & \alpha \gets \frac{\alpha_0 + \alpha_1}{2}, \\ & \text{if $f(\alpha_0)f(\alpha) > 0$} \\ & \qquad \alpha_0 \gets \alpha, \\ & \text{else} \\ & \qquad \alpha_1 \gets \alpha, \\ & \text{end} \end{aligned}\]

So the algorithm checks in each step where the sign change occurred and moves the $\alpha_0$ or $\alpha_1$ accordingly. The loop is terminated if config.linesearch_max_iterations is reached (by default 60 for Float64 in the Options struct, see linesearch_iterations); in that case a warning is emitted (at verbosity ≥ 1) and the best estimate found so far is returned.

Warning

The obvious danger with using bisections is that the supplied interval can have multiple roots (or no roots). One should be careful to avoid this when fixing the interval.

Info

Bisection can only locate a root if the endpoints straddle a sign change. If the endpoints have the same sign there is no (odd-multiplicity) root in the interval. Rather than erroring — a line search must not abort the enclosing solve — bisection returns the endpoint closest to a root (smallest |f|) and reports the failure: _bisection_core distinguishes it from a located root with BISECTION_NOBRACKET, and bisection warns accordingly.

source
SimpleSolvers.bracketMethod
bracket(f, x, bc, s, k, nmax, αmax)

Grow a bracket outward from x (in steps scaled by k, starting from s) until the BracketingCriterion bc is satisfied. Used by bracket_minimum and bracket_root.

Extended help

Before entering the main loop we check whether the criterion is already satisfied just to the left of a (at a - s). This early exit is only valid for the BracketRootCriterion, where it corresponds to a sign change in (a - s, b). For the BracketMinimumCriterion it would instead bracket a maximum rather than a minimum, so it is deliberately skipped.

Returns nothing when no bracket is found within nmax steps. A line search must be able to report an unbracketable merit rather than abort the enclosing solve, so this is a nothing rather than an error; see bracket_minimum.

αmax bounds how far to the right the search may probe (see linesearch_αmax); a bracket truncated by it is returned like any other, and only _bracket_core distinguishes the two.

source
SimpleSolvers.bracket_minimumMethod
bracket_minimum(f, x)

Move a bracket successively in the search direction (starting at x) and increase its size until a local minimum of f is found.

This is used in bisections when only one x is given (and not an entire interval).

This bracketing algorithm is taken from [3]. Also compare it to bracket_minimum_with_fixed_point.

Arguments

Extended help

For bracketing we need two constants $s$ and $k$ (see DEFAULT_BRACKETING_s and DEFAULT_BRACKETING_k).

Before we start the algorithm we initialize it, i.e. we check that we indeed have a descent direction:

\[\begin{aligned} & a \gets x, \\ & b \gets a + s, \\ & \mathrm{if} \quad f(b) > f(a)\\ & \qquad\text{Flip $a$ and $b$ and set $s\gets-s$.}\\ & \mathrm{end} \end{aligned}\]

The algorithm then successively computes:

\[c \gets b + s,\]

and then checks whether $f(c) \geq f(b)$ (also see BracketMinimumCriterion). If this is true it returns $(a, c)$ or $(c, a)$, depending on whether $a<c$ or $c<a$ respectively. If this is not satisfied $a,$ $b$ and $s$ are updated:

\[\begin{aligned} a \gets & b, \\ b \gets & c, \\ s \gets & sk, \end{aligned}\]

and the algorithm is continued. If we have not found a bracket after $n_\mathrm{max}$ iterations (see DEFAULT_BRACKETING_nmax) the algorithm terminates and returns nothing. The interval that is returned by bracket_minimum is then typically used as a starting point for bisection.

Returns `nothing` on failure

A line search must be able to report a merit it cannot bracket rather than abort the enclosing solve, so an unbracketable f yields nothing rather than an error. Callers must handle it — see solve_with_status and LinesearchOutcome.

Info

The function bracket_root is equivalent to bracket_minimum with the only difference that the criterion we check for is:

\[f(c)f(b) < 0,\]

i.e. that a sign change in the function occurs. Also see BracketRootCriterion.

source
SimpleSolvers.bracket_minimum_with_fixed_pointMethod
bracket_minimum_with_fixed_point(f, x, s, k, nmax, αmax)

Find a bracket while keeping the left side (i.e. x) fixed.

The algorithm is similar to bracket_minimum (also based on DEFAULT_BRACKETING_s and DEFAULT_BRACKETING_k) with the difference that for the latter the left side is also moving.

The function bracket_minimum_with_fixed_point is used as a starting point for Quadratic (adapted from [6]): that line search fits the polynomial centred at the bracket's left endpoint $a$ (which may differ from the input x after the bracketer’s initial direction flip),

\[p(\alpha) = f(a) + f'(a)(\alpha - a) + p_2(\alpha - a)^2,\]

so interpolating $f$ at the right endpoint $b$ fixes the coefficient $p_2$ as

\[p_2 = \frac{f(b) - f(a) - f'(a)(b - a)}{(b - a)^2},\]

where $(a, b)$ is the bracket returned by $\mathtt{bracket\_minimum\_with\_fixed\_point}$. The right end b is grown outward (with the left end a held fixed) until f stops decreasing, i.e. until the turning point f(b) ≥ f(b_\mathrm{prev}) is reached, so that a minimum is bracketed in (a, b). (The earlier variant compared against the fixed anchor f(a) instead, which failed to bracket a minimum whose right tail stays below f(a).) The Quadratic caller guards the fitted curvature (p_2 ≤ 0 falls back to a bisection step), so f(b) > f(a) is no longer required.

Returns the bracket together with the function values at its endpoints, (a, b, f(a), f(b)) with a < b. The values are already computed during bracketing, so the caller (the Quadratic line search) does not have to re-evaluate f at the endpoints.

Returns nothing if no bracket is found within nmax steps — a line search must be able to report an unbracketable merit rather than abort the enclosing solve.

αmax bounds how far to the right the search may probe; see linesearch_αmax and _bracket_minimum_with_fixed_point_core, which is what tells a truncated bracket from a genuine one.

source
SimpleSolvers.cacheMethod
cache(ls)

Return the cache of the LinearSolver.

Examples

For the default LU(), a small matrix (leading dimension ≤ N_STATIC_THRESHOLD) is stored as a mutable static matrix (MMatrix):

julia> ls = LinearSolver(LU(), [1.0 2.0; 3.0 4.0]);

julia> cache(ls)
SimpleSolvers.LUSolverCache{Float64, StaticArraysCore.MMatrix{2, 2, Float64, 4}}([1.0 2.0; 3.0 4.0], [0, 0], [0, 0], 0)

Passing static=false forces a plain Matrix cache regardless of size:

julia> ls = LinearSolver(LU(; static=false), [1.0 2.0; 3.0 4.0]);

julia> cache(ls)
SimpleSolvers.LUSolverCache{Float64, Matrix{Float64}}([1.0 2.0; 3.0 4.0], [0, 0], [0, 0], 0)
source
SimpleSolvers.capped_statusMethod
capped_status(prob, params, αmax, φ₀, d₀, τ, n=0)

The LinesearchStatus of a search whose bracketing reached the ceiling αmax (see linesearch_αmax) with the merit still falling. The turning point then lies beyond the largest step the caller allows, so αmax is the best admissible step, and this is the shared definition of what to report about it.

It is deliberately not a failure and not a distinct LinesearchOutcome. The merit is evaluated at αmax and classified by exactly the rule every other returned step is classified by: LINESEARCH_DECREASED when it beats $\varphi(0)$ by more than the round-off allowance $\tau$, LINESEARCH_FLOOR when it does not. That is honest in both directions — on a compact merit, where this case arises, $\varphi(\alpha_\mathrm{max})$ is genuinely lower and the step genuinely decreases the merit — and a caller that wants to know whether its ceiling bound the search can compare the ceiling it supplied against steplength.

n is what the bracketing that reached the ceiling spent, which the caller gets from the bracketing core (see _bracket_core). It has to be passed in rather than assumed, because on this path the bracketing is the search: reporting only the single evaluation made here would make trials say a capped search cost one step whatever it actually cost.

source
SimpleSolvers.change_precisionMethod
change_precision(T, method::LinesearchMethod)

Return a copy of the LinesearchMethod method with its numeric fields converted to the element type T.

This is an internal helper used when constructing a Linesearch: the method's precision is adapted to the working precision T. It replaces a former misuse of Base.convert (which was ambiguous with Base and violated the convert contract by returning a differently-typed object).

source
SimpleSolvers.check_anchorMethod
check_anchor(φ₀, d₀, α, αmax=Inf)

Validate the $\alpha = 0$ anchor of a line search problem. Return a LinesearchStatus that the caller should return immediately, or nothing if the anchor is usable and the search may proceed.

This is the one definition of the anchor policy shared by every LinesearchMethod:

  • $\varphi(0)$ or $\varphi'(0)$ not finite, or $\varphi'(0) > 0$, gives LINESEARCH_NO_DESCENT: no $\alpha$ can decrease the merit along this direction, so shrinking or bracketing would only spend merit evaluations to discover that. The caller's trial step α is handed back — never the $\alpha = 0$ anchor, which would freeze the outer iterate (x .+= 0 .* d).
  • $\varphi'(0) = 0$ gives LINESEARCH_STATIONARY. For the $\|F\|^2$ merit of a NonlinearSolver this is the exact root ($F = 0 \Rightarrow \varphi'(0) = 0$ and the direction vanishes), so it is benign and every $\alpha$ is equivalent.

An ascent anchor arises in practice when the direction did not come from an exact, freshly factorized Newton solve — a stale Jacobian under refactorize > 1, a nonzero regularization_factor, or an inexact linear solve. The correct response is to refresh the Jacobian, which is why the line search reports the situation instead of trying to salvage a step from it, and solver_step! acts on the report: on LINESEARCH_NO_DESCENT it leaves the iterate where it is (moving along a direction that cannot decrease the merit would only make the retry start from a worse point) and records a stall, which forces a fresh Jacobian on the next step (see needs_refresh and maybe_refactorize!) and gives up after max_stalls if that does not help.

The step handed back is therefore still positive, as the contract requires — whether to use it is the caller's decision, not the line search's. It also respects αmax (see linesearch_αmax), so that the ceiling holds on every return of a line search and not only on the ones that searched.

source
SimpleSolvers.check_gradientMethod
check_gradient([io], g)

Check norm, maximum value and minimum value of a vector.

Output is written to io (defaulting to stdout).

Examples

julia> g = [1., 1., 1., 2., 0.9, 3.];

julia> SimpleSolvers.check_gradient(g; digits=3)
norm(Gradient):               4.1
minimum(|Gradient|):          0.9
maximum(|Gradient|):          3.0
source
SimpleSolvers.check_hessianMethod
check_hessian([io], H)

Check the condition number, determinant, max and min value of the Hessian H.

Output is written to io (defaulting to stdout).

Info

Here the Hessian H is a matrix and not of type Hessian.

julia> H = [1. √2.; √2. 3.];

julia> SimpleSolvers.check_hessian(H)
Condition Number of Hessian: 13.9282
Determinant of Hessian:      1.0
minimum(|Hessian|):          1.0
maximum(|Hessian|):          3.0
source
SimpleSolvers.check_jacobianMethod
check_jacobian([io], J)

Check the condition number, determinant, max and min value of the Jacobian J.

Output is written to io (defaulting to stdout).

Info

Here the Jacobian J is a matrix. It is not a Jacobian object.

julia> J = [1. √2.; √2. 3.];

julia> SimpleSolvers.check_jacobian(J)
Condition Number of Jacobian: 13.9282
Determinant of Jacobian:      1.0
minimum(|Jacobian|):          1.0
maximum(|Jacobian|):          3.0
source
SimpleSolvers.checkfactorizedMethod
checkfactorized(lsolver::LinearSolver{T,<:PivotedLUMethod})

Throw an ArgumentError if factorize! has not been called on lsolver yet.

The PivotedLUMethod counterpart of the perms[1] == 0 guard in LU's ldiv!: without it, getrs would be handed an all-zero pivot vector and return garbage rather than complain.

source
SimpleSolvers.checkjacobianprototypeMethod
checkjacobianprototype(jacobian, jacobian_prototype)

Throw an ArgumentError if a sparse jacobian_prototype is paired with a Jacobian that cannot write into one.

A sparse prototype is a promise that the Jacobian will be assembled with a fixed sparsity pattern, and the two Jacobians the package builds for itself cannot keep it: JacobianAutodiff hands ForwardDiff a dense matrix and JacobianFiniteDifferences fills one column at a time, so both would write to structurally-zero positions. Caught here, at construction, rather than inside jacobian! on the first iteration.

Those two are named rather than JacobianFunction being allow-listed, so that a caller's own Jacobian subtype — which knows perfectly well how to assemble into a fixed pattern — is not refused for not being one of ours.

source
SimpleSolvers.checkpatternMethod
checkpattern(lsolver, A)

Throw a DimensionMismatch unless A matches the size the cache was built for.

The pattern itself is checked by the backend, which is where the useful error lives: both are told the pattern may not change, because the ordering and symbolic factorization the cache holds were computed for one.

source
SimpleSolvers.checksparseMethod
checksparse(method, A)

Throw an ArgumentError unless A is a square SparseMatrixCSC.

The SparseDirectMethods need the sparsity pattern, and handing one a dense matrix is a mistake worth naming rather than silently converting: converting would build a SparseMatrixCSC with no structural zeros, whose factorization is slower than LapackLU's in every measurement.

source
SimpleSolvers.compute_new_iterate!Method
compute_new_iterate!(xₖ₊₁, xₖ, αₖ, pₖ)

Compute xₖ₊₁ based on a direction pₖ and a step length αₖ.

Extended help

In the case of vector spaces this function simply does:

xₖ = xₖ + αₖ * pₖ

For manifolds we instead perform a retraction [7].

source
SimpleSolvers.convert_αmaxMethod
convert_αmax(T, αmax)

Convert the step-length ceiling αmax to the precision T, saturating a finite value at floatmax(T) instead of letting it overflow to Inf.

Inf is passed through unchanged: it is not an overflow but a statement — "no ceiling of my own" (see linesearch_αmax) — and turning it into floatmax(T) would impose one the caller declined. Used by change_precision for the αmax field of every method that has one.

source
SimpleSolvers.copy_matrix!Method
copy_matrix!(dest, src)

Copy src into dest, preserving dest's storage.

The sparse-aware replacement for dest .= src. Sparse-to-sparse copies the stored values only, and requires the two patterns to be identical — a differing pattern is an error rather than a silent reallocation, because the solver cache built from dest holds an ordering and a symbolic factorization computed for dest's pattern, and quietly changing it underneath would either be wrong or turn every step into a fresh O(nnz log nnz) ordering.

source
SimpleSolvers.curvature_diagnosticMethod
curvature_diagnostic(status, ls, params)

Method-specific extra diagnostic emitted by linesearch_warnings at verbosity ≥ 2. The fallback does nothing; Backtracking checks the CurvatureCondition, which costs a derivative evaluation — a full Jacobian for the line search problem of a NonlinearSolver, hence the verbosity gate.

Reached only from a direct solve call, since that is the only caller of linesearch_warnings. A NonlinearSolver at verbosity = 2 therefore no longer pays for this once per iteration; to see it for a step of a solve, call the line search on that step's problem directly.

source
SimpleSolvers.default_linear_solver_methodMethod
default_linear_solver_method(A)

The LinearSolverMethod to use for a system whose matrix looks like A.

This is what NewtonSolver and DogLegSolver fall back on when no linear_solver_method is given, and it dispatches on the Jacobian prototype rather than on the element type alone, because the right answer depends on the storage as much as on the number type:

Amethod
dense, Float32/Float64/ComplexF32/ComplexF64LapackLU
dense, anything else — BigFloat, Rational, …LU
sparse, Float64/ComplexF64UmfpackLU
sparse, anything elsenone — an ArgumentError

A sparse matrix is never densified for you. UmfpackLU, the sparse default, solves Float64 and ComplexF64 systems only — UMFPACK converts a 32-bit matrix in lu/lu! but has no 32-bit solve, and it does not handle BigFloat or Rational at all. For every other element type this raises, naming the two things a caller might have meant: SparspakLU, which is generic in the element type and keeps the matrix sparse, or a dense method that discards the sparsity.

Densifying is a real answer — it is often the right one for a small matrix — but it throws away the structure the caller went to the trouble of building, and it is a decision that belongs to them rather than to a fallback. Choosing SparspakLU instead is not available to a default either: it lives in a package extension, so a default that reached for it would work or fail depending on what the caller had imported. So this asks. Once asked, both answers work: pass either as linear_solver_method.

LU used to be the default everywhere. It is still the one for a dense matrix of an element type LAPACK does not know, and the right choice for a very small system, where its static-matrix cache allocates nothing. (A Rational or Integer matrix reaches it and is then refused by lucache_eltype, which names the conversion to make — the package has no dense method for those at all, and saying so is better than a default that pretends otherwise.)

But its allocation-free MMatrix path stops at N_STATIC_THRESHOLD = 10 — and at any size for a non-isbitstype element type, see _static — and above that it is a scalar triple loop with no blocking. Measured on an Apple M4 Max it is 2× slower than LapackLU at n = 64 and 32× slower at n = 768, with its triangular solve a further 3.5–4.5× behind getrs throughout. A caller who did not know to pass linear_solver_method paid all of that; downstream, that was 74 % of an implicit time step.

RecursiveLU is never selected automatically: it lives in a package extension, its useful range depends on which BLAS is loaded, and it is not always installed. Choose it explicitly.

source
SimpleSolvers.default_linesearch_αmaxMethod
default_linesearch_αmax(T)

DEFAULT_LINESEARCH_αmax in the precision T, saturated at floatmax(T).

The saturation is the whole point of the function. 65536 exceeds floatmax(Float16) = 65504, so Float16(DEFAULT_LINESEARCH_αmax) is Inf — a Float16 line search built from the plain conversion would carry no ceiling at all, which is precisely the defect the field exists to fix, absent in the one precision where every other tolerance in this file is special-cased.

default_linesearch_αmax(Float16)

# output

Float16(6.55e4)
source
SimpleSolvers.default_toleranceMethod
default_tolerance(T)

Determine the default tolerance for a specific data type. This is used in the constructor of Options.

Compare this to default_precision.

Examples

julia> default_tolerance(Float64)
4.440892098500626e-16
julia> default_tolerance(Float32)
2.3841858f-7
julia> default_tolerance(Float16)
Float16(0.001953)
source
SimpleSolvers.default_ϵMethod
default_ϵ(::Type{T})

The default step size on whose basis finite differences are computed, for the working precision T. Used by GradientFiniteDifferences and JacobianFiniteDifferences.

Its value is $8\sqrt{\varepsilon_T}$, where $\varepsilon_T$ is the machine epsilon of T. Being precision-aware (eps(T), not a baked-in Float64 epsilon) is essential for Float32 finite differences to be accurate.

Examples

julia> default_ϵ(Float64)
1.1920928955078125e-7
julia> default_ϵ(Float32)
0.0027621358f0
source
SimpleSolvers.directions!Method
directions!(s, x, params, iteration=1; force_refactorize=false)

Compute direction₁ and direction₂ for the DogLegSolver.

This is equivalent to direction! for the NewtonSolver.

Examples

julia> J = [0 1; -1 0];

julia> f(y, x, params) = y .= cos.(J * x .- 2.) .^ 2 / l2norm(sin.(x) .- 1.);

julia> x = zeros(2); y = similar(x); s = DogLegSolver(x, y; F = f);

julia> directions!(s, x, NullParameters());

julia> direction₁(cache(s))
2-element Vector{Float64}:
 -0.25513686072399455
  0.1601152321012896

julia> round.(direction₂(cache(s)); digits = 11)
2-element Vector{Float64}:
 -0.22882877718
  0.22882877718

The rounding is not decoration. direction₂ comes out of the LU solve, so its last bit carries the accumulation order of whichever BLAS kernel is underneath — and that varies by machine, not by Julia version. Mathematically the two components of this one are exact negatives; printed to sixteen significant digits they are -0.22882877718014286 and 0.22882877718014286 on aarch64 (identically on Julia 1.10, 1.12 and 1.13) and -0.22882877718014286 and 0.22882877718014288 on x86_64 OpenBLAS, which is what CI runs on. Asserting that last digit is asserting the noise rather than the result. direction₁ above needs no such treatment: it is mul! and norms, and never touches the factorization.

Extended help

The Gauss-Newton direction (i.e. direction₂) is computed the usual way:

\[\mathbf{d}_2 = -\mathbf{J}^{-1} \mathbf{r}\]

where $\mathbf{J}$ is the Jacobian matrix and $\mathbf{r}$ is the residual vector. The steepest descent direction (taken from [1, Equation (11.46)]) is different:

\[\mathbf{d}_1 = -\frac{||\mathbf{J}^T\mathbf{r}||^2}{\mathbf{r}^T(\mathbf{J}\mathbf{J}^T)(\mathbf{J}\mathbf{J}^T)\mathbf{r}}\mathbf{J}^T\mathbf{r}.\]

The DogLegSolver then interpolates between these two directions (this interpolation is piecewise linear).

As for the (quasi-)NewtonSolver, the Jacobian is only re-evaluated and refactored every refactorize iterations (see DogLeg), and always on a fresh state or the first step (iteration ≤ 1), or when force_refactorize = true (used by solver_step! to recover from a collapsed trust-region radius, and after any step that did not move the iterate — see needs_refresh). In between, the stale Jacobian and its factorization are reused for both directions. The default refactorize = 1 refactorizes on every step.

source
SimpleSolvers.dogleg_direction!Method
dogleg_direction!(cache, Δ)

Compute the (piecewise-linear) dogleg step for trust-region radius Δ from the steepest-descent direction direction₁ and the Newton direction direction₂ (both already stored in cache), writing the result into direction(cache).

direction₁ and direction₂ do not depend on Δ, so this may be called repeatedly while shrinking Δ without recomputing (and refactorizing) the Jacobian.

source
SimpleSolvers.dominant_linesearch_outcomeFunction
dominant_linesearch_outcome(status, count_floor=true)

The non-isbenign LinesearchOutcome the line search reported most often during the solve, or nothing if it reported none. This is what nonlinear_solver_warnings names when it explains a solve that did not converge: a solve whose line search failed does so for one reason nearly every time, and naming that one reason is more use than a histogram.

Ties go to the outcome declared first in LinesearchOutcome, which orders them from the benign end (LINESEARCH_FLOOR, the merit is simply irreducible) towards the actionable one (LINESEARCH_NO_DESCENT, the direction is wrong and the Jacobian is the suspect) — so a tie is broken away from the more alarming diagnosis rather than towards it.

count_floor = false skips LINESEARCH_FLOOR, which is how a converged solve is asked whether anything went wrong: reaching the merit's round-off floor on the last step is how a solve converges, so for that question the floor is not a failure at all. It is the tie rule above that makes the distinction matter — a converged solve that floored once and was exhausted once would otherwise be explained by the floor, which is the half of it that is expected.

source
SimpleSolvers.factorizationMethod
factorization(lsolver::LinearSolver{T,<:PivotedLUMethod})

A LinearAlgebra.LU view of the factorization held in the cache.

This wraps the cache's arrays rather than copying them, so it is only valid until the next factorize!. Neither ldiv! nor singular_index goes through it — building it allocates, and they are on the hot path — but it is what to reach for when a LinearAlgebra.Factorization is what you want, e.g. det(factorization(lsolver)).

source
SimpleSolvers.factorize!Method
factorize!(lsolver::LinearSolver{T,<:PivotedLUMethod}[, A])

Factorize in place in cache(lsolver).A, with whichever kernel the method selects — see _getrf!. With two arguments A is first copied into the cache; with one, whatever the cache already holds is factorized.

The factorization is not checked here. A singular matrix is reported when the factorization is used, by ldiv!, so that a caller that factorizes speculatively — as a quasi-Newton method does — is not interrupted by a matrix it may never solve with.

Warning

As for LU, the factorization overwrites cache(lsolver).A with the factors, so the single-argument form is good for exactly one call; calling it twice would factorize the factors. Use the two-argument form to refactorize.

source
SimpleSolvers.factorize!Method
factorize!(lsolver::LinearSolver, A)

Factorize the matrix A and store the result in cache(lsolver).A.

Note that calling cache on lsolver returns the instance of LUSolverCache stored in lsolver.

Examples

julia> A = [1. 2. 3.; 5. 7. 11.; 13. 17. 19.]
3×3 Matrix{Float64}:
  1.0   2.0   3.0
  5.0   7.0  11.0
 13.0  17.0  19.0

julia> x = zeros(3);

julia> lsolver = LinearSolver(LU(; static=false), x);

julia> factorize!(lsolver, A).cache.A
3×3 Matrix{Float64}:
 13.0        17.0       19.0
  0.0769231   0.692308   1.53846
  0.384615    0.666667   2.66667

julia> y = A * ldiv!(x, lsolver, ones(3));

julia> round.(y; digits = 10)
3-element Vector{Float64}:
 1.0
 1.0
 1.0

Here cache(lsolver).A stores the factorized matrix. If we call factorize! with two input arguments as above, the method first copies the matrix A into the LUSolverCache. We can equivalently also do:

julia> lsolver = LinearSolver(LU(), A);

julia> factorize!(lsolver).cache.A
3×3 StaticArraysCore.MMatrix{3, 3, Float64, 9} with indices SOneTo(3)×SOneTo(3):
 13.0        17.0       19.0
  0.0769231   0.692308   1.53846
  0.384615    0.666667   2.66667

Note the difference between the output types of the two refactorized matrices: the default LU() chose a mutable static (MMatrix) cache because the matrix is small (see _static and N_STATIC_THRESHOLD), whereas LU(; static=false) forced a plain Matrix.

Also see ldiv! for how the refactorized matrix is used.

source
SimpleSolvers.factorize!Method
factorize!(lsolver::LinearSolver{T,UmfpackLU}, A)

Refactorize A, reusing the ordering and symbolic factorization already in the cache.

SparseArrays' lu! reuses the ordering and symbolic factorization the cache holds, which is the SparseFactorizationCache contract, and raises ArgumentError: pattern of the matrix changed if the pattern does not match the one they were computed for — which is why the error worth reading comes from the backend and checkpattern only checks the size. Keeping the pattern fixed is still the caller's job, and the reason LinearSolver construction takes the prototype.

source
SimpleSolvers.fill_nan!Method
fill_nan!(A)

Fill A with NaNs.

For a sparse matrix only the stored entries are filled. That is not a shortcut: for a sparse Jacobian the sparsity pattern is structural information the linear solver depends on — the ordering and symbolic factorization in a SparseFactorizationCache were computed for one pattern — so clearing has to preserve it. fill!(A, NaN) would throw for a SparseMatrixCSC anyway, since NaN is not the structural zero.

Used wherever a cache or problem is initialized or cleared: clear!(::LinearProblem) and the initialize! methods of NonlinearSolverCache and DogLegCache.

source
SimpleSolvers.flag_stall!Method
flag_stall!(state)

Record that the line search of the current step reported that it cannot make progress along the current direction — either the merit is at its round-off floor (isfloor) or the anchor is not a descent direction at all (LINESEARCH_NO_DESCENT, see LinesearchOutcome). The flag is OR-ed into the verdict of the next record_stall!, which clears it again, and it makes needs_refresh true for the next step.

This is how a line search that knows it cannot help reports one iteration earlier than the step-based diagnosis of stalled_step — which remains the primary mechanism, since it is the only one that also covers a Static step along an underflowed direction, a collapsed DogLegSolver trust-region radius, and a locally expanding PicardSolver map.

source
SimpleSolvers.havenonfiniteMethod
havenonfinite(status)

Check whether any of the three residuals of a NonlinearSolverStatusrxₛ, rfₐ, rfₛ — is not finite, i.e. whether the iteration has left the region where the problem is representable. Used by meets_stopping_criteria to give up and by nonlinear_solver_warnings to say so.

The test is isfinite, not !isnan: a residual that has overflowed is as unusable as an undefined one, and the pure-NaN test used to miss it entirely — neither this predicate nor the rfₐ > f_abstol_break gate fired for an infinite residual, since f_abstol_break defaults to Inf and Inf > Inf is false, so such a solve ran its whole max_iterations budget with no diagnosis at all. The same widening was made to the solver-side guards; see nan_recovery!.

Note that a status is never converged by accident here — every comparison with NaN is false and no infinite residual passes residual_small — so this predicate decides when to stop and what to report, not whether the answer is good.

source
SimpleSolvers.isbenignMethod
isbenign(oc)

true for the LinesearchOutcomes that report no failure: LINESEARCH_DECREASED (a genuine decrease), LINESEARCH_STATIONARY (nothing to search for) and LINESEARCH_UNKNOWN (the method does not report one). The remaining three — LINESEARCH_FLOOR, LINESEARCH_EXHAUSTED and LINESEARCH_NO_DESCENT — are what linesearch_failures counts and what linesearch_warnings reports on.

LINESEARCH_FLOOR counts as a failure here even though it is the expected final state of a converged solve, because whether it matters is the outer iteration's call and this is how the outer iteration is told: the tally is read by nonlinear_solver_warnings, which names it only for a solve that did not converge.

source
SimpleSolvers.isfloorMethod
isfloor(status)

true if the line search could not find any step that changes the merit by more than the round-off allowance τ, i.e. the merit has reached its round-off floor. The outer iteration cannot make progress in this state no matter how the step is chosen — which is why a NonlinearSolver counts it as a stalled step (see record_stall!).

source
SimpleSolvers.isnotprogressingMethod
isnotprogressing(status)

Check whether the iteration has been given up on for lack of progress: config.f_stall_window iterations without the residual dropping by config.f_stall_factor, see no_progress. Always false at the default f_stall_window = 0.

Mutually exclusive with isconverged, for the same reason isstalled is: the criterion requires the residual not to be small, whereas both convergence branches require that it is.

As with isstalled, whether this counts as failure is the caller's decision: the solve reached status.rfₐ and could not do better within the window it was given.

source
SimpleSolvers.isstalledMethod
isstalled(status, config)

Check whether the iteration has stagnated: config.max_stalls consecutive steps stalled_step.

Mutually exclusive with isconverged — a stalled step is by definition one whose residual is not small, whereas both convergence branches require that it is.

A stagnated solve has reached the numerical floor of its residual and cannot improve it. Whether that counts as success is the caller's decision, which is why the status is queryable (see status): if status.rfₐ is acceptable to you, treat isstalled as success — and consider raising f_abstol above it, since the tolerance you asked for is not attainable.

source
SimpleSolvers.issufficientMethod
issufficient(status)

true if the line search found a step with a genuine sufficient decrease, i.e. one that decreased the merit by more than the round-off allowance τ of status. Compare isfloor.

source
SimpleSolvers.iterate_settledMethod
iterate_settled(rxₛ, config, state)

Return true when the last step did not move the iterate, rxₛ ≤ ‖x‖·x_suctol. Used by assess_convergence and stalled_step.

An infinite step never counts as settled, even though Inf ≤ ‖x‖·x_suctol holds once ‖x‖ has overflowed too: an iterate that jumped to infinity has neither converged nor frozen, it has broken down, and that is what havenonfinite is for.

source
SimpleSolvers.iterations_since_progressMethod
iterations_since_progress(state)

Return the number of iterations since the residual last dropped by config.f_stall_factor, as recorded in state::NonlinearSolverState by record_progress!. Zero on a freshly initialized state.

This measures the failure mode stall_number cannot see. A stalled step is one that did not move the iterate; here the iterate moves perfectly normally — by far more than the round-off level of $x$ — while the residual descends towards a floor above the requested tolerance, so neither stalled_step nor either branch of assess_convergence can fire and the solve spends max_iterations in full. nonlinear_solver_warnings reports it, and no_progress stops it when the caller has opted in with f_stall_window.

source
SimpleSolvers.linearsolverMethod
linearsolver(solver)

Return the linear part (i.e. a LinearSolver) of an NewtonSolver.

Examples

x = rand(3)
y = rand(3)
F(x) = tanh.(x)
F!(y, x, params) = y .= F(x)
s = NewtonSolver(x, y; F = F!)
linearsolver(s)

# output

LinearSolver{Float64, LapackLU, SimpleSolvers.PivotedLUCache{Float64, Matrix{Float64}}}(LapackLU(), SimpleSolvers.PivotedLUCache{Float64, Matrix{Float64}}([NaN NaN NaN; NaN NaN NaN; NaN NaN NaN], [0, 0, 0], 0, false))
source
SimpleSolvers.linesearch_iterationsMethod
linesearch_iterations(T)

Determine the default number of trial steps a line search may take, i.e. the default of the linesearch_max_iterations field of Options.

This is deliberately not the same quantity as max_iterations, which bounds the outer nonlinear iteration (see meets_stopping_criteria). A one-dimensional search inside a single solver step needs a budget on the order of the mantissa width, not thousands of trials: a Backtracking ladder $\alpha \gets p\alpha$ starting at $\alpha_0 = 1$ reaches the negligible-step floor after $\lceil-\log_2\varepsilon\rceil$ halvings (52 in double precision, 24 in single), and a bisection needs the same count to exhaust the mantissa. We take that count plus a small margin; everything beyond it can only produce denormals.

The count is derived for the default shrink factor $p = 0.5$. A Backtracking built with a p close to 1 needs more trials in principle, though in the case that matters — a merit frozen at its round-off floor — the safeguarded interpolation shrinks by about a half per trial regardless of p, because the quadratic model through a frozen value has its minimiser at $\alpha/2$.

Quadratic and BierlaireQuadratic are bounded by the same field even though they fit a quadratic rather than shrink a step: they converge on their own ε tolerance long before the budget, which serves only as a backstop, so there is no reason for them to carry a separate knob.

Compare this to default_tolerance and absolute_tolerance.

Examples

julia> linesearch_iterations(Float64)
60
julia> linesearch_iterations(Float32)
31
source
SimpleSolvers.linesearch_outcomesMethod
linesearch_outcomes(status)

Return the tally of LinesearchOutcomes the line search reported during the solve, indexed by linesearch_index. See record_linesearch! for why this, and not the log, is how a caller learns that the line search was rejected.

Examples

julia> F(y, x, params) = y .= x .^ 2 .- 2;

julia> x = [1.0];

julia> st = solve_with_status!(x, NonlinearProblem(F, zero(x)), Newton(); verbosity = 0);

julia> linesearch_outcomes(st)[linesearch_index(LINESEARCH_NO_DESCENT)]
0
source
SimpleSolvers.linesearch_problemMethod
linesearch_problem(nl::NonlinearSolver)

Build a line search problem based on a NonlinearSolver.

Producing a single-valued output

We apply L2norm to the output of value! (the evaluation of the nonlinear problem). This is because the solver operates on a function with array-valued outputs from which we have to find roots (in contrast to an optimizer which operates on a function with a scalar output of which we should find a minimum).

Examples

We show how to set up the LinesearchProblem for a simple example and compute $f^\mathrm{ls}(\alpha_0)$ and $\partial{}f^\mathrm{ls}/\partial\alpha(\alpha_0)$.

julia> F(y, x, params) = y .= (x .- 1.).^2;

julia> x = ones(3)/2; y = similar(x); nl = NewtonSolver(x, y; F = F);

julia> _params = NullParameters();

julia> direction!(nl, x, _params, 1)
3-element Vector{Float64}:
 0.25
 0.25
 0.25

julia> ls_prob = linesearch_problem(nl);

julia> state = NonlinearSolverState(x); update!(state, x, F(y, x, _params));

julia> params = (parameters = _params, x = state.x)
(parameters = NullParameters(), x = [0.5, 0.5, 0.5])

julia> ls_prob.F(0., params)
0.1875

julia> ls_prob.D(0., params)
-0.375
source
SimpleSolvers.linesearch_problemMethod
linesearch_problem(nlp, jacobian, cache)

Make a line search problem for a Newton solver (the cache here is an instance of NonlinearSolverCache).

Extended help

The line search closures evaluate the merit at trial steps α using private scratch buffers rather than the solver's shared cache. The shared buffers (solution/value/jacobianmatrix) are read by the solver after the line search returns (e.g. the next direction! step and the convergence check), so writing trial iterates into them would be an aliasing hazard. The line search therefore only reads the current direction from the shared cache and the current iterate from params.x; every write goes to a closure-owned buffer.

params may carry an optional φ₀ field holding the merit at the $\alpha = 0$ anchor. Every line search evaluates that anchor first, and it is exactly the residual the solver has already computed at the current iterate, so solver_step! passes it along and saves one F evaluation per solver step — the most expensive single operation for a large residual. A caller who drives solver_step! by hand from a state whose value is stale must not supply it.

It may carry an optional αmax field too, the caller's ceiling on the step length; see linesearch_αmax, which documents why that one has to be per call. That one is read by the method rather than by these closures, but through the same hasproperty guard, resolved from the parameter type at compile time, so supplying neither costs nothing. params therefore has to answer property access throughout — a NamedTuple or any struct, which is what the required x and parameters fields already demanded.

source
SimpleSolvers.linesearch_reasonFunction
linesearch_reason(status, config, oc=dominant_linesearch_outcome(status))

The clause nonlinear_solver_warnings appends to explain a failed solve in terms of what its line search reported, or "" when it reported nothing but success. oc is the outcome to explain — by default the dominant one, and for a converged solve the dominant one other than LINESEARCH_FLOOR, which is what made that message fire (see dominant_linesearch_outcome); the clause has to name the outcome the caller acted on, not a different one that happens to be as frequent.

This is the only thing said about the line search during a solve. A line search does not log from inside the iteration — it reports to the solver, which tallies the outcomes (see record_linesearch!) — so without this clause a solve that stagnated because every one of its steps was rejected would name the symptom and not the cause. A count is what makes it evidence: "the line search rejected 194 of 200 steps" is a diagnosis, "the line search failed once" is noise from the last step of an otherwise healthy solve.

Like no_progress_reason and linesearch_exhausted_reason, this is called from inside the @warn message so the string is built only for a message that is actually shown.

source
SimpleSolvers.linesearch_warningsFunction
linesearch_warnings(status, ls, params=NullParameters())

Report a LinesearchStatus obtained from solve_with_status. Compare this to nonlinear_solver_warnings. This is the only place where a line search emits log messages, so every LinesearchMethod reports identically.

LINESEARCH_FLOOR and LINESEARCH_STATIONARY are reported only at verbosity ≥ 2, because both are the expected final state of a converged solve — a residual that cannot be improved because it is already as small as the arithmetic allows.

Who this is for

This function is reached from solve and from nowhere else, which is what makes it safe for it to report unconditionally. A line search has two callers and owes them different things:

So there is nothing here to rate limit, and none of these messages carries a maxlog. They used to, because solver_step! called this function once per iteration and a solve that cannot make progress asks the line search for an impossible decrease at every one of them — thousands of identical messages. But maxlog is keyed on the source location of the @warn, so the caps were process-global and were never reset between solve! calls: once spent, they were spent for the rest of the session, and a genuine line-search failure in a later solve of a long run was silent. Not reporting from inside the loop removes the flood at its source, so the caps are gone and nothing goes permanently silent.

Whether an irreducible merit actually matters is the outer iteration's call, and nonlinear_solver_warnings makes it: it reports stagnation once, naming the residual that was achieved, the tolerance that was requested, and what the line search reported along the way.

The messages themselves live in report_linesearch_status rather than here, which is a compile-time rather than a stylistic decision — see its docstring before merging them back.

source
SimpleSolvers.linesearch_αmaxMethod
linesearch_αmax(method, params)

The ceiling on the step length a LinesearchMethod may return: the smaller of the method's own method_αmax and the caller's params.αmax, if the caller supplied one. This is the single definition of that policy, as check_anchor is of the anchor policy, and every built-in method calls it once before it searches.

Why the caller's half has to be per call

A bracketing search grows its bracket outward until the merit stops falling, so where $\varphi$ is nearly flat — or its minimiser genuinely far away — the step it returns is bounded only by the bracketing budget: $2^{100}$ times the initial step, in principle. In a Euclidean problem that is self-correcting, since $\varphi$ grows with $\alpha$ and the search's own decrease test throws such a step out. On a compact manifold it is not: $\varphi$ is bounded there and can be genuinely lower at $\alpha = 10^9$ than at $\alpha = 0$, so nothing the search can measure calls the step too large. The merit is not a bound on the step, and the bound that does exist belongs to the caller — for a manifold solver, the $2\pi$ of a rotation, divided by the norm of the direction, which changes at every solver step.

Hence the two halves. method_αmax is a backstop that costs nothing and needs no knowledge of the problem; params.αmax is how a caller with a scale of its own imposes it, and it is read from params rather than taken as a keyword so that the extension point solve_with_status(ls, α, params) keeps its signature — a third-party method that does not know about the ceiling still compiles, and one that does needs only this call. It is the same channel params.φ₀ uses (see linesearch_problem), and the hasproperty guard is resolved at compile time, so a caller that supplies nothing pays nothing.

Example

solve_with_status(ls, one(T), (x = x, parameters = params, αmax = 2π / norm(direction)))

The other fields are the ones the merit closures of linesearch_problem read; αmax is additional and independent of them.

A ceiling is not a failure

A search that stops at the ceiling returns it, with the merit measured there and classified by the usual round-off allowance $\tau$. It is a LINESEARCH_DECREASED if the merit really did fall; see DEFAULT_LINESEARCH_αmax.

An invalid ceiling raises

A params.αmax that is not positive, or is NaN, is a caller error rather than a situation arising from the merit, and raises an ArgumentError before a single evaluation is spent — as the method constructors do for their own parameters. This is the one deliberate exception to the never-throws clause of LinesearchMethod, which is about the problem and not about the call. Inf is not an error: it says the caller has no scale of its own, and leaves the method's ceiling in place.

source
SimpleSolvers.lucache_eltypeMethod
lucache_eltype(T)

The element type used by the LUSolverCache for an input matrix of element type T. Linear solves are only supported for floating-point problems — real (AbstractFloat) or complex (Complex{<:AbstractFloat}) — so any other element type (e.g. an integer or rational matrix) is rejected here with a clear error rather than silently promoted. For a supported type the cache uses T unchanged.

source
SimpleSolvers.lucache_matrixMethod
lucache_matrix(static, A, Tf)

The working matrix for a LUSolverCache: an MMatrix if static, else a plain Matrix.

An explicit static = true for a non-isbitstype element type is an ArgumentError here rather than StaticArrays' setindex! failure later; the default never asks for it, see _static.

Note the Matrix in the non-static branch, rather than a broadcast that would preserve the input's storage. A sparse A would otherwise give a sparse cache, and factorize!'s scalar loops write to positions that are structurally zero — so it would fail deep inside the factorization, a long way from the cause. LapackLU densifies for the same reason. Use UmfpackLU or SparspakLU to actually exploit sparsity.

Densifying only ever happens because the caller asked for LU by name: default_linear_solver_method never selects a dense method for a sparse matrix, it raises instead.

source
SimpleSolvers.maybe_refactorize!Method
maybe_refactorize!(s, x, params, iteration; force=false, stalled=false)

Re-evaluate the Jacobian at x, copy it into the LinearProblem (adding the diagonal regularization_factor), and refactorize the LinearSolver — but only on a refactorization step: a fresh state or the first step (iteration ≤ 1), every refactorize iterations (see Newton), when the previous step made no progress (stalled, see needs_refresh), or when forced (used by the DogLegSolver to recover from a collapsed trust-region radius). Otherwise the stale Jacobian and its factorization are reused (quasi-Newton). Returns the solver s.

stalled is what makes the quasi-Newton mode safe to combine with max_stalls: a step that did not move the iterate would otherwise rebuild the same direction from the same stale Jacobian on the next refactorize - 1 iterations and reproduce the same negligible step, so the solve would be given up on (see stalled_step) for a reason a fresh Jacobian could have fixed. Refreshing immediately means the second consecutive stall is one that a fresh Jacobian did not fix, which is the conclusive evidence max_stalls = 2 assumes. It is also the response check_anchor prescribes for an ascent anchor, which is a stale-Jacobian symptom.

source
SimpleSolvers.meets_stopping_criteriaMethod
meets_stopping_criteria(state, config)

Determines whether the iteration stops based on the current NonlinearSolverState.

Warning

The function meets_stopping_criteria may return true even if the solver has not converged. To check convergence, call assess_convergence (with the same input arguments).

The function meets_stopping_criteria returns true if one of the following is satisfied:

  • the status::NonlinearSolverStatus is converged (checked with isconverged) and state.iterations ≥ config.min_iterations,
  • the status has stagnated (checked with isstalled, i.e. config.max_stalls consecutive steps that did not move the iterate while the residual is not small) and state.iterations ≥ config.min_iterations,
  • the status is making no progress (checked with isnotprogressing, i.e. config.f_stall_window iterations without the residual dropping by config.f_stall_factor while it is not small) and state.iterations ≥ config.min_iterations; this is opt-in and never fires at the default f_stall_window = 0,
  • status.f_increased and config.allow_f_increases = false (i.e. f increased even though we do not allow it),
  • state.iterations ≥ config.max_iterations,
  • status.rfₐ > config.f_abstol_break (by default Inf). In theory this returns true if the residual gets too big.
  • one of the residuals (rxₛ, rfₐ, rfₛ) is not finite (checked with havenonfinite) and state.iterations ≥ 1,

So convergence is only one possible criterion for which meets_stopping_criteria. We may also satisfy a stopping criterion without having convergence!

Examples

In the following example we show that meets_stopping_criteria evaluates to true when used on a freshly allocated NonlinearSolverStatus:

julia> config = Options(verbosity=0);

julia> x = [NaN, 2., 3.]
3-element Vector{Float64}:
 NaN
   2.0
   3.0

julia> f = [NaN, 10., 20.]
3-element Vector{Float64}:
 NaN
  10.0
  20.0

julia> cache = NonlinearSolverCache(x, copy(x));

julia> state = NonlinearSolverState(x);

julia> update!(state, x, f); state.iterations += 1
1

julia> status = NonlinearSolverStatus(state, config);

julia> meets_stopping_criteria(state, config)
true

This obviously has not converged. To check convergence we can use assess_convergence. ```

source
SimpleSolvers.method_αmaxMethod
method_αmax(method)

The largest step length the LinesearchMethod method will try of its own accord, i.e. its αmax field where it has one. The fallback is Inf, which is what Backtracking returns: it shrinks the caller's trial step, so the trial step is already its ceiling, and its opt-in expansion phase carries its own bound (q^nexpand).

Bisection, Quadratic, BierlaireQuadratic and StrongWolfe each have the field, defaulting to DEFAULT_LINESEARCH_αmax. Use linesearch_αmax rather than this, which is only half of the ceiling.

source
SimpleSolvers.minimum_decrease_thresholdMethod
minimum_decrease_threshold(T)

The minimum value by which a function $f$ should decrease during an iteration.

The default value of $10^{-4}$ is often used in the literature [4], [1].

Examples

julia> minimum_decrease_threshold(Float64)
0.0001
julia> minimum_decrease_threshold(Float32)
0.0001f0
source
SimpleSolvers.nan_recovery!Method
nan_recovery!(s, x, params)

Damp direction(cache(s)) by nan_factor until the trial iterate x + d has a finite residual (or the nan_max_iterations budget of dampings is exhausted). On return solution(cache(s)) and value(cache(s)) hold the last trial iterate and its residual, and direction(cache(s)) is the direction that trial was built from — so value(cache(s)) is F(x + direction(cache(s))) on every exit path, which is what the Picard solver_step! reads instead of re-evaluating F. One trial is always evaluated, so a budget of zero refreshes the cache without damping at all. Used by the generic and Picard solver_step!s. Returns the solver s.

"Finite" means isfinite, not merely "not NaN": a residual that has overflowed is as unusable as an undefined one and is just as much a symptom of a step that left the region where F is representable — the -1/|x| of issue #130 gives Inf at x = 0, not NaN. The option names (nan_factor, nan_max_iterations) predate that and are kept for compatibility.

Damping is only meaningful because the direction is known to be finite: the callers reject a non-finite one outright (see solver_step!), and they must, since Inf * nan_factor is Inf and the loop would spend its whole budget reproducing the same trial iterate.

source
SimpleSolvers.needs_refreshMethod
needs_refresh(state)

true when the previous step made no progress, i.e. when a stall has been flagged for the current step (flag_stall!) or the consecutive-stall counter is nonzero (stall_number).

solver_step! passes this to maybe_refactorize! as its stalled keyword, so a quasi-Newton solver rebuilds its Jacobian immediately after a step that did not move the iterate instead of waiting for the next refactorize multiple. Both sources are consulted because record_stall! consumes the flag into the counter once per iteration, and a caller who drives solver_step! by hand may never call it.

source
SimpleSolvers.no_progressMethod
no_progress(rfₐ, config, state)

Return true when the iteration has spent config.f_stall_window iterations without the residual dropping by config.f_stall_factor (see iterations_since_progress) while the residual is not small (see residual_small). Always false at the default f_stall_window = 0, which disables the criterion — see F_STALL_WINDOW for why it is opt-in.

This is the sibling of stalled_step for a solve whose iterate has not frozen. Both end an iteration that cannot reach the requested tolerance, and they cover disjoint cases: stalled_step fires when the step has dropped below the round-off level of $x$, so that the merit cannot be reduced along the current direction; no_progress fires when the steps are perfectly healthy and the residual is descending — just towards a floor above the tolerance, slowly enough that the remaining budget cannot get there. Their thresholds differ by orders of magnitude for the same reason: two consecutive stalled steps are conclusive because the second one had a fresh Jacobian, whereas no number of moving steps is conclusive about a rate, which is why one is a default and the other a policy the caller sets.

The !residual_small gate is the same one stalled_step carries, and it is what keeps giving up and converging mutually exclusive: a residual that has stopped improving because it is already small enough is success, and assess_convergence says so.

source
SimpleSolvers.nonlinear_solver_warningsMethod
nonlinear_solver_warnings(status, config)

Report a NonlinearSolverStatus at the end of a solve!: the iteration count if it reached warn_iterations, stagnation at the residual floor (see isstalled and stalled_step), a lack of progress (see spent_without_progress and isnotprogressing), a disallowed residual increase, a residual beyond f_abstol_break, and non-finite residuals (see havenonfinite). Compare this to linesearch_warnings, which does the same for the inner line search, and to print_status.

All messages except the iteration count and the two hard-failure ones are gated on config.verbosity ≥ 1.

The three "this solve did not do what you asked" messages are mutually exclusive, most specific first: stagnation (the iterate froze) wins over lack of progress (the iterate moves but the residual is going nowhere), which in turn replaces the bare iteration count — which on its own names a symptom and no cause, and was the only thing a non-progressing solve used to report.

The line search reports here, not from inside the loop

A line search emits nothing during a solve: it reports to the solver through the LinesearchStatus that solve_with_status returns, and solver_step! tallies the outcomes (see record_linesearch!). So the two failure messages above carry the clause linesearch_reason builds, which names the outcome the line search reported most often and how often — the cause behind the symptom they otherwise report on their own. A solve that converged anyway says the same thing at verbosity ≥ 2, as an @info, and only for a failure that is not LINESEARCH_FLOOR: the last step of a converged solve reaches the merit's round-off floor as a matter of course, so counting that would report every healthy solve.

To act on the outcome rather than read about it, use solve_with_status! and the tally on the returned NonlinearSolverStatus; see linesearch_outcomes.

Rate limiting

The three repeatable messages are gated on should_report!, which reports the 1st, 2nd, 4th, 8th … occurrence of a diagnosis rather than the first three and then nothing ever again. The keys of the two diagnoses carry the dominant line-search outcome, so a solve that starts failing for a new reason is reported at once; the bare iteration count has no cause to key on and uses a plain one. The trade-off — a repeating diagnosis is not reported on every occurrence — is spelled out in that docstring. verbosity = 0 still silences the solver completely.

source
SimpleSolvers.print_jacobianMethod
print_jacobian([io], J)

Display the Jacobian J as an aligned text/plain table.

Output is written to io (defaulting to stdout).

Info

Here the Jacobian J is a matrix. It is not a Jacobian object.

source
SimpleSolvers.print_statusMethod
print_status(status, config)

Print the solver status if:

  • config.verbosity $\geq1$ and one of the following three
  1. the solver is converged,
  2. status.iterations ≥ config.max_iterations,
  3. status.iterations ≥ config.warn_iterations
  • config.verbosity $>1.$
source
SimpleSolvers.record_iteration!Method
record_iteration!(state, config)

Take the two per-iteration measurements of state::NonlinearSolverState — the consecutive-stall counter (record_stall!) and the progress reference (record_progress!) — from a single evaluation of residuals.

This is the one function carrying the "exactly once per iteration" contract that both counters depend on: solve! calls it right after update!, and nothing else does. Both counters are increments rather than predicates, so calling it twice would double-count and never calling it leaves both at zero, which is exactly how a hand-rolled iteration that drives solver_step! directly behaves.

Sharing the residuals is why it exists at all: the two recordings need rxₛ and rfₐ between them, and computing them once here rather than once in each keeps a per-iteration norm off the hot loop.

source
SimpleSolvers.record_linesearch!Method
record_linesearch!(state, oc)

Count one LinesearchOutcome oc into the tally of state::NonlinearSolverState. Called once per solver_step! that consults a line search, and — like record_stall! and record_progress! — a measurement rather than a predicate, so it must be called exactly once per such step.

This is the channel by which a line-search failure reaches the user. A line search reports to its caller through the LinesearchStatus that solve_with_status returns and emits nothing; only a caller who invoked solve directly is a user, and only that path goes through linesearch_warnings. A NonlinearSolver is not that caller — it consumes the status (flag_stall!, and it declines to step along a LINESEARCH_NO_DESCENT direction) and accumulates it here, so that the solve explains itself once, at the end, through nonlinear_solver_warnings, instead of once per iteration.

That is not a cosmetic difference. A solve that cannot make progress asks the line search for an impossible decrease at every one of its iterations, so reporting per iteration turned a single diagnosis into thousands of identical messages — which is what the maxlog caps used to hold back, at the price of being keyed on source location and therefore process-global: once spent, they stayed spent for the rest of the session, and a genuine failure in a later solve was silent. Not reporting from inside the loop removes the flood at its source, so no cap is needed and nothing goes permanently silent.

source
SimpleSolvers.record_progress!Method
record_progress!(state, config)
record_progress!(state, config, rfₐ)

Update the progress reference of state::NonlinearSolverState: when the residual has dropped to config.f_stall_factor times the residual of the last iteration that counted as progress, that becomes the new reference and the count returned by iterations_since_progress restarts; otherwise the count advances by one. Returns that count. The three-argument form takes a residual rfₐ the caller has already computed; the two-argument form computes it from value(state).

The reference is therefore monotonically non-increasing — it is the best residual so far, at the granularity of the factor — which is what makes the measurement immune to a residual that jumps around: an iteration that undoes the progress of the previous one does not reset the clock. See F_STALL_FACTOR for the choice of granularity.

Like record_stall!, this is a per-iteration measurement rather than a predicate, so it must be called exactly once per iteration; record_iteration! is what does so, and carries that contract. That is why the counter lives here and not in NonlinearSolverStatus, which is pure and is built more than once per iteration. Because the count is a field rather than a difference of iteration numbers, a hand-rolled iteration that drives solver_step! directly and never records keeps it at zero and behaves exactly as before — the same guarantee stall_number gives.

source
SimpleSolvers.record_stall!Method
record_stall!(state, config)
record_stall!(state, config, rxₛ, rfₐ)

Update the consecutive-stall counter of state::NonlinearSolverState: increment it when the last step stalled_step or the line search flagged a stall (see flag_stall!), and reset it to zero otherwise. The flag is cleared either way. Returns the new count (see stall_number). The four-argument form takes residuals the caller has already computed; the two-argument form computes them from the state.

This is a per-iteration measurement rather than a predicate, so it must be called exactly once per iteration; record_iteration! is what does so, and carries that contract. That is why the counter is not maintained inside assess_convergence or NonlinearSolverStatus: those are pure and are evaluated more than once per iteration, so incrementing there would double-count. A hand-rolled iteration that drives solver_step! directly and never records simply keeps the count at zero and behaves exactly as before.

source
SimpleSolvers.report_linesearch_statusMethod
report_linesearch_status(status, name, config)

Emit the messages for a LinesearchStatus; the reporting half of linesearch_warnings, whose docstring documents the verbosity policy and who it is for.

Implementation

This is a function barrier, and its signature is what makes it one. linesearch_warnings is called from solve — every direct call to a line search, and nothing else since solver_step! stopped reporting per iteration — and takes a Linesearch, which carries the closure types of its LinesearchProblem, and a NamedTuple of parameters. So it is specialized once per problem a line search is built for. A message in its body is specialized with it, and all of the Base.CoreLogging and string-interpolation code that @warn expands to is re-inferred and re-codegen'd for each one, which on a caller that builds one line search per tableau dominates the cost of the calls themselves.

Taking name and config, and nothing whose type can vary per solver, bounds the specializations of this function to one per element-type combination for the whole session. nonlinear_solver_warnings and print_status have the same shape for the same reason.

So: do not give this function a parameter whose type varies per solver, and do not move the messages back into linesearch_warnings. test/linesearch_tests.jl asserts both — the first from the method signature, which bounds the specialization set rather than sampling it, and the second by scanning the lowered code of each function for Base.CoreLogging.

The @noinline is a guard rather than the mechanism: Julia's inliner refuses a body this size anyway, but a future one that is more willing would undo the barrier, and nothing in the caller wants this inlined.

The element types are deliberately not tied together as LinesearchStatus{T}/Options{T}: this is a reporting path, and a precision mismatch anywhere upstream should not turn a diagnostic into a MethodError that replaces the problem being diagnosed. nonlinear_solver_warnings is written the same way.

source
SimpleSolvers.reset_warning_counts!Method
reset_warning_counts!()

Forget every count kept by should_report!, so that the next occurrence of each diagnosis is reported again.

Mostly for tests: unlike maxlog — which Test.TestLogger sees straight through, since the suppression happens in the logger — a message the backoff suppresses is never emitted at all, so a test that asserts on a message has to start from a known count.

source
SimpleSolvers.residual_smallMethod
residual_small(rfₐ, config, state)

Return true when the absolute residual rfₐ passes the standard $\mathrm{atol} + \mathrm{rtol}\cdot\|F(x_0)\|$ residual test,

\[r^f_a \leq \texttt{f\_abstol} + \texttt{f\_reltol}\cdot\|F(x_0)\|,\]

with $\|F(x_0)\|$ the initial_residual of state. This lets a large-magnitude or ill-conditioned solve converge once its residual is reduced by f_reltol from $\|F(x_0)\|$, while a step that stalls near $\|F(x_0)\|$ still fails. The relative term drops to zero until the state has been initialized (initial_residual is NaN), leaving the pure absolute f_abstol test.

This gate is shared by assess_convergence, which requires it in addition to a successive-change criterion, and by stalled_step, which requires its negation: a frozen iterate is convergence when the residual is small and stagnation when it is not. The two are therefore mutually exclusive by construction.

source
SimpleSolvers.residualsMethod
residuals(state)

Compute the residuals for state::NonlinearSolverState. The computed residuals are the following:

  • rxₛ : successive residual (the norm of $x - \bar{x}$),
  • rfₐ: absolute residual in $f$,
  • rfₛ : successive residual (the norm of $y - \bar{y}$).
source
SimpleSolvers.resolve_jacobianMethod
resolve_jacobian(F, DF!, jacobian, x, y)

Resolve the Jacobian for a nonlinear-solver constructor: an explicit DF! wins (wrapped as a JacobianFunction), otherwise an explicit jacobian, otherwise a lazily-built JacobianAutodiff. Building the autodiff Jacobian lazily avoids allocating a ForwardDiff config when either DF! or a jacobian is supplied.

source
SimpleSolvers.resolve_linear_solver_methodMethod
resolve_linear_solver_method(linear_solver_method, A)

Resolve the LinearSolverMethod for a nonlinear-solver constructor: an explicit one wins, missing falls back to default_linear_solver_method for the Jacobian prototype A.

By dispatch rather than by coalesce, which would evaluate the fallback even when it is not needed — and the fallback is allowed to throw (a sparse 32-bit float has no defensible default), so an explicit method has to reach the solver without it being consulted at all.

source
SimpleSolvers.should_report!Method
should_report!(key)

Count one occurrence of the diagnosis key and return true when it should be reported, i.e. on its 1st, 2nd, 4th, 8th, 16th … occurrence. Used by nonlinear_solver_warnings in place of the maxlog keyword of @warn.

Extended help

Why not maxlog

A NonlinearSolver reports at most once per solve!, which is the right rate for a caller that solves once and far too high a one for a caller that solves in a loop — a time-stepping integrator asking for a tolerance its problem cannot attain would get the same message at every step. maxlog bounds that, but it is keyed on the source location of the @warn, so its budget is process-global and is never reset: once spent, the message is gone for the remainder of the session, including for later solves of entirely different problems. A genuine failure late in a long run was therefore silent.

What the backoff promises, and what it does not

Doubling gives $O(\log N)$ messages over $N$ solves — a handful over a run of any length — while never reaching a point beyond which nothing is ever said again. Be clear about the two halves of that:

  • a diagnosis appearing for the first time is reported at once, whichever solve it happens in, because its counter is still at zero. This is the case maxlog got wrong, and it is the one that matters: a run that was healthy for ten thousand steps and then was not says so.
  • a diagnosis that keeps repeating is reported on solves 1, 2, 4, 8, 16 … of its own count. Occurrence 10 is silent; occurrence 16 is not. Every occurrence is not promised, and asking for that would be asking for the flood back.

Keys should therefore be as specific as the distinctions worth waking the user for — which is why the caller folds the dominant LinesearchOutcome into the key of a stagnation report: a solve that starts stagnating for a new reason is a new diagnosis, and is reported immediately rather than inheriting the suppressed counter of the old one.

verbosity = 0 remains the way to silence a solver completely, and NonlinearSolverStatus remains the way to act on an outcome rather than read about it.

source
SimpleSolvers.singular_indexMethod
singular_index(lsolver)

Return the index of the first zero pivot encountered by factorize!, or 0 if the factorization succeeded.

This is the one piece of factorization state that callers outside the linear solver need: ldiv! turns a non-zero index into a SingularException, and the DogLegSolver reads it to decide whether the Newton leg of the step is available at all (see SimpleSolvers.directions!). Every LinearSolverMethod therefore has to implement it; going through cache(lsolver) directly would tie those callers to one method's cache layout.

Calling this before factorize! is an error, not 0 — an unfactorized solver is not a non-singular one.

source
SimpleSolvers.singular_indexMethod
singular_index(lsolver::LinearSolver{T,<:PivotedLUMethod})

The zero-pivot index the factorization reported (LAPACK's info), or 0 if it succeeded.

source
SimpleSolvers.singular_indexMethod
singular_index(lsolver::LinearSolver{T,<:SparseDirectMethod})

0 if the factorization succeeded, non-zero if it did not.

Not an index

Neither sparse backend reports which pivot vanished, so unlike LU/LapackLU this is a flag widened to the interface's return type, not a position. For SparspakLU it is worse than that: singularity is only detected when the factorization is used, so this returns 0 until a ldiv! has failed. See the SparspakLU docstring.

source
SimpleSolvers.solve!Method
solve!(x, ls::LinearSolver, lsys::LinearProblem)

Solve the LinearProblem lsys with the LinearSolver ls and store the result in x.

Also see solve(::LU, ::AbstractMatrix, ::AbstractVector).

Examples

julia> x = zeros(3)
3-element Vector{Float64}:
 0.0
 0.0
 0.0

julia> A = [1.; 0.; 0.;; 0.; 2.; 0.;; 0.; 0.; 4.]
3×3 Matrix{Float64}:
 1.0  0.0  0.0
 0.0  2.0  0.0
 0.0  0.0  4.0

julia> b = ones(3)
3-element Vector{Float64}:
 1.0
 1.0
 1.0

julia> ls = LinearSolver(LU(), x);

julia> problem = LinearProblem(x); update!(problem, A, b);

julia> solve!(x, ls, problem)
3-element Vector{Float64}:
 1.0
 0.5
 0.25
source
SimpleSolvers.solve!Method
solve!(x, prob, method, args...; kwargs...)

Solve the NonlinearProblem prob with the NonlinearSolverMethod method, starting from x and overwriting it with the solution (which is returned).

The remaining positional arguments are passed on to solve!(::AbstractArray, ::NonlinearSolver, ::NonlinearSolverState, params), so they are either nothing at all, the params of the problem, or a NonlinearSolverState followed by params.

The keyword arguments are the Options keywords together with whatever the constructor selected by method accepts — which is not the same set for all four methods:

methodconstructor keywords
Newton, QuasiNewtonlinesearch, jacobian, linear_solver_method
DogLegjacobian, linear_solver_method
Picardjacobian

Picard and DogLeg consult no line search, so they reject a linesearch keyword rather than ignoring it (it falls through to Options and raises a MethodError there) — see PicardSolver(::AbstractVector{T}, ::NonlinearProblem, ::AbstractVector{T}) where {T}.

refactorize is deliberately absent from that table: it belongs to the method, as Newton(5) (equivalently QuasiNewton) or DogLeg(5). Passing it as a keyword does work for those two — it is forwarded after method.refactorize and, being the rightmost occurrence, silently wins — but Picard has no Jacobian to refactorize and errors on it. Configure it through method.

Info

This is the convenience path: every call builds a solver — a Jacobian (with its ForwardDiff configuration), a factorization cache and the line-search buffers. Code that solves repeatedly should construct one NonlinearSolver and call solve!(::AbstractArray, ::NonlinearSolver, ::NonlinearSolverState, params) on it instead.

solve! returns the solution, not a status; use solve_with_status! if the outcome of the solve is needed.

Examples

julia> F(y, x, params) = y .= x .^ 2 .- 2;

julia> prob = NonlinearProblem(F, zeros(1));

julia> x = [1.0];

julia> solve!(x, prob, Newton(); verbosity = 0)
1-element Vector{Float64}:
 1.4142135623730951
source
SimpleSolvers.solveMethod
solve(x, prob, method, args...; kwargs...)

Solve the NonlinearProblem prob with the NonlinearSolverMethod method, starting from the initial guess x, and return the solution as a new array — x itself is left untouched. This is solve!(::AbstractVector, ::NonlinearProblem, ::NonlinearSolverMethod) on a copy, and takes the same arguments; the note on solver construction there applies here too.

The initial guess has to be passed because a NonlinearProblem stores neither the solution nor the residual, so nothing else determines the size and type of the array to allocate.

Examples

julia> F(y, x, params) = y .= x .^ 2 .- 2;

julia> prob = NonlinearProblem(F, zeros(1));

julia> x₀ = [1.0];

julia> solve(x₀, prob, Newton(); verbosity = 0)
1-element Vector{Float64}:
 1.4142135623730951

julia> x₀
1-element Vector{Float64}:
 1.0
source
SimpleSolvers.solveMethod
solve(lu, A, b)

Solve the linear problem determined by A and b.

This is the most straightforward way to solve this system.

Examples

julia> A = [1.; 0.; 0.;; 0.; 2.; 0.;; 0.; 0.; 4.]
3×3 Matrix{Float64}:
 1.0  0.0  0.0
 0.0  2.0  0.0
 0.0  0.0  4.0

julia> b = ones(3)
3-element Vector{Float64}:
 1.0
 1.0
 1.0

julia> solve(LU(), A, b)
3-element Vector{Float64}:
 1.0
 0.5
 0.25

Note that the result is a plain Vector even though the cache for a matrix this small is an MMatrix: the solution vector comes from alloc_rhs, which is deliberately dense and deliberately not derived from the cache's storage.

Compare this to solve!(::AbstractVector, ::LinearSolver, ::LinearProblem).

source
SimpleSolvers.solveMethod
solve(ls::LinearSolver, args...)

Counterpart of solve! for a prebuilt LinearSolver: allocates (and returns) a fresh solution vector instead of writing into a caller-supplied one. Note that the solver's cache is still updated in place (the factorization is computed there).

Accepts the same trailing arguments as solve!(ls, args...): a LinearProblem, a matrix-vector pair A, b, or a bare right-hand side b (the latter uses the factorization already stored in ls).

source
SimpleSolvers.solveMethod
solve(method::SparseDirectMethod, ls::LinearProblem)
solve(method::SparseDirectMethod, A, b)

Allocate a LinearSolver, factorize and solve in one call.

For a one-off system only: this pays the ordering and symbolic factorization every time, which is most of the cost of a sparse solve. Inside a loop, build the LinearSolver once and call factorize! and ldiv! on it — that is what reuses the symbolic phase.

source
SimpleSolvers.solveMethod
solve(linesearch, α, params=NullParameters())

Solve the LinesearchProblem (contained in Linesearch) starting at α, report the outcome through linesearch_warnings and return the step length.

The argument params needs to be of an appropriate form expected by the respective LinesearchProblem.

Use solve_with_status to obtain the LinesearchStatus instead: a caller that has to tell "I found a decreasing step" from "the merit is at its round-off floor and nothing can decrease it" cannot do so from the step length alone — and it is the call that emits no messages, which is what a program wants (see record_linesearch!).

See linesearch_problem.

Implementation

This is derived, and it is the only definition: a LinesearchMethod implements solve_with_status, and solve is that plus the report. It used to be the other way round — every method defined this same three-line body, and a method that defined only solve got solve_with_status from a fallback that called it. That fallback made the layering of the contract unenforceable: a third-party method reached through solve emits its messages from wherever it is called, including from inside every iteration of a NonlinearSolver, which is precisely what a program must not see. With the direction reversed, there is no path by which the package calls a method's solve during a solve, so the guarantee holds by construction rather than by convention.

α is converted to the element type of the Linesearch here, as the problem-taking method above does, so solve(ls, 1) on a Linesearch{Float64} means what it looks like it means.

source
SimpleSolvers.solve_with_status!Function
solve_with_status!(x, s, params=NullParameters())
solve_with_status!(x, s, state, params=NullParameters())
solve_with_status!(x, prob, method, args...; kwargs...)

Solve as solve! does — x is overwritten with the solution — but return the NonlinearSolverStatus instead of x. It is the same iteration: both share one body, so the status is the one the solve built to report on itself rather than a second one computed after the fact.

The first and third forms are how the outcome of a solve is obtained without holding on to a NonlinearSolverState: they build one and hand it to status afterwards. The ! is part of the name because x is modified, unlike in the line-search solve_with_status, whose α is a number.

The second form takes the caller's own state rather than building one, and is the form for a loop. The other two allocate a state per call, which is the objection the prob/method wrapper already carries for the solver itself: a caller stepping through time should build one NonlinearSolver and one NonlinearSolverState and reuse both. It is otherwise identical, and it leaves the state holding the outcome, so a later status(s, state) returns what this returned — the two ways of reading one solve cannot disagree.

The args... of the third form are what solve!(::AbstractVector, ::NonlinearProblem, ::NonlinearSolverMethod) takes: nothing at all, the params of the problem, or a NonlinearSolverState followed by params. The keywords are that method's too, and the note there on solver construction applies here — a call that passes a state but no solver still builds one per call, and a loop wants the second form.

Info

Neither the returned status's predicates — isconverged and isstalled — nor status, which the second form's promise above is about, are exported, for the same reason the line-search predicates are not: they are generic names a package doing using SimpleSolvers may well want for itself. Reach them as SimpleSolvers.isconverged(st), or import them explicitly with using SimpleSolvers: isconverged, status.

Examples

julia> F(y, x, params) = y .= x .^ 2 .- 2;

julia> prob = NonlinearProblem(F, zeros(1));

julia> x = [1.0];

julia> isconverged(solve_with_status!(x, prob, Newton(); verbosity = 0))
true

The same solve through a reused solver and state, which is the form a loop wants — and the state still answers for it afterwards:

julia> F(y, x, params) = y .= x .^ 2 .- 2;

julia> x = [1.0]; s = NewtonSolver(x, similar(x); F = F, verbosity = 0);

julia> state = SolverState(s);

julia> st = solve_with_status!(x, s, state);

julia> isconverged(st)
true

julia> status(s, state) == st
true
source
SimpleSolvers.solve_with_statusMethod
solve_with_status(ls, α, params=NullParameters())

Like solve, but return a LinesearchStatus — the step length plus the reason the search stopped — and emit no log messages. Use linesearch_warnings to report the status; that is all solve adds.

This is what a program calls, and it is the LinesearchMethod extension point: every built-in method (all six of Backtracking, StrongWolfe, Bisection, Quadratic, BierlaireQuadratic and Static) implements this, and gets solve derived from it. A method that reports no outcome of its own returns LINESEARCH_UNKNOWN, as Static does.

A method must implement this

There is no fallback: the generic method below raises rather than deriving a status from solve. It used to do exactly that, and the derivation ran the wrong way — a method that defined only solve was then reached through solve from inside every iteration of a NonlinearSolver, and emitted its messages there, which is the one thing the contract in LinesearchMethod promises does not happen. Deriving solve from this instead makes that promise structural. A third-party method that defines only solve therefore has to move its body here; the boilerplate it used to carry (solve_with_status, then linesearch_warnings, then steplength) is what it gets for free in exchange.

source
SimpleSolvers.solver_step!Method
solver_step!(x, s, state, params)

Compute one step for solving the problem stored in an instance s of NonlinearSolver.

Examples

julia> f(y, x, params) = y .= sin.(x .- .5) .^ 2
f (generic function with 1 method)

julia> x = ones(3) / 4
3-element Vector{Float64}:
 0.25
 0.25
 0.25

julia> y = zero(x)
3-element Vector{Float64}:
 0.0
 0.0
 0.0

julia> s = NewtonSolver(x, similar(x); F = f);

julia> state = NonlinearSolverState(x); update!(state, x, f(y, x, NullParameters()));

julia> solver_step!(x, s, state, NullParameters())
3-element Vector{Float64}:
 0.37767096061051814
 0.37767096061051814
 0.37767096061051814
source
SimpleSolvers.solver_step!Method
solver_step!(x, s::PicardSolver, state, params)

Take one fixed-point (Picard) step $x \gets x + \alpha d$ with the residual direction $d = -F(x)$ (see direction!).

Unlike a Newton/Gauss-Newton step, the Picard direction $d = -F(x)$ is not in general a descent direction for the merit $\varphi = \|F\|^2$, so applying the derivative-based (Wolfe) line search used by the other NonlinearSolvers is inappropriate (a directional derivative that is not negative makes the sufficient- decrease/curvature tests meaningless).

Instead the step is damped by a residual-monotonicity backtracking: starting from the full fixed-point step $\alpha = 1$ the step is halved until the residual norm does not increase, $\|F(x + \alpha d)\| \le \|F(x)\|$. This safeguard uses only function values and makes no descent assumption. If no positive $\alpha$ reduces the residual (the fixed-point map is locally expanding), the smallest trial step is taken and the convergence test — which requires a small residual, not merely a small step — correctly reports non-convergence instead of a false positive.

source
SimpleSolvers.spent_without_progressMethod
spent_without_progress(status)

Check whether the iteration failed to converge and spent at least half of its iterations, and at least F_STALL_REPORT_MINIMUM of them, without the residual dropping by config.f_stall_factor — the diagnosis nonlinear_solver_warnings reports when a solve has used its whole budget, and the condition under which the no-progress line is shown by show.

This is not gated on any option, unlike isnotprogressing, and it can afford not to be because it is only ever used to describe a solve, never to decide one: nonlinear_solver_warnings consults it about a solve that has already spent max_iterations, and show about one it has been handed to print. A threshold that would be reckless as a stopping criterion (see F_STALL_WINDOW) is harmless as an explanation — which is why the two exist separately.

Both guards are here rather than at the call sites because show has no Options and so cannot apply them itself, and it is the caller most exposed to a false positive. isconverged is the primary one: a residual that stopped improving because it was already small enough is success, and a solve held to a large min_iterations would otherwise spend most of its iterations on a converged plateau and start explaining itself. The absolute minimum is the backstop for a solve that has not converged and is simply short — without it the proportion alone is satisfied by a two-iteration solve whose last step did not halve the residual. With both, no healthy solve comes close: a Gauss(2) Lotka-Volterra run converges in two to four iterations with at most one of them unproductive.

A long healthy solve does not reach it either, for the separate reason that its residual keeps halving: an iteration converging linearly with rate $\rho$ halves every $-1/\log_2\rho$ iterations, 69 of them even at $\rho = 0.99$.

source
SimpleSolvers.stalled_stepMethod
stalled_step(rxₛ, rfₐ, config, state)

Return true when the last step stalled: it left the iterate unchanged (see iterate_settled) while the residual is not small (see residual_small).

A stalled step is the failure mode that the residual gate in assess_convergence correctly refuses to call convergence, and that used to be invisible to the solver. The step length $\alpha\|d\|$ has dropped below the round-off level of $x$, so the merit $\|F\|^2$ cannot be reduced along the current direction — typically because the requested f_abstol lies below the round-off floor of the residual itself. Taking another step recomputes the same direction and the same negligible $\alpha$, so the iteration would spin all the way to max_iterations, asking the line search on every one of those steps to improve a residual that is already pure round-off noise.

meets_stopping_criteria therefore stops after config.max_stalls consecutive stalled steps (counted by record_stall!), and nonlinear_solver_warnings reports the achieved residual against the requested tolerance instead of the misleading "Solver took 1000 iterations.".

Info

The condition is deliberately phrased in terms of the step actually taken rather than a line-search return code. It is the same diagnosis for a Backtracking ladder that exhausted, a StrongWolfe search that found no acceptable step, a Static step along an underflowed direction, a DogLegSolver whose trust-region radius collapsed and a PicardSolver whose fixed-point map is locally expanding. A line search that knows it is at the round-off floor can report one iteration earlier via flag_stall!.

source
SimpleSolvers.statusMethod
status(solver, state)

Return the NonlinearSolverStatus for the NonlinearSolverState state as assessed with the Options of solver.

solve! returns the solution x (updated in place), not a status, so this is how a caller inspects the outcome of a solve — in particular whether it converged (isconverged) or merely stagnated at the residual floor (isstalled). The state is the caller's own object (it is passed to solve!), so nothing has to be threaded back out of the solve.

Examples

julia> F(y, x, params) = y .= x .^ 2 .- 2;

julia> x = [1.0]; s = NewtonSolver(x, similar(x); F = F, verbosity = 0);

julia> state = SolverState(s);

julia> solve!(x, s, state);

julia> isconverged(status(s, state))
true
source
SimpleSolvers.triple_point_finderMethod
triple_point_finder(f, x)

Find three points a < b < c (strictly ordered in position) with f(a) ≥ f(b) and f(c) > f(b), so that a minimum is bracketed in (a, c). This is used for performing a quadratic line search (see BierlaireQuadratic). Returns a Symbol instead of a triple when no such triple exists — see the warning below.

Note

The left inequality is non-strict (f(a) ≥ f(b)): while descending, consecutive samples may tie on a plateau, and for a flat-bottomed f a strict f(a) > f(b) is unattainable. f(b) is still strictly below f(c), and BierlaireQuadratic guards a degenerate (collinear) fit by falling back to a bisection step, so the non-strict left bound is sufficient to bracket the minimum.

Searches rightward only, and reports failure as a `Symbol`

Unlike bracket_minimum, which flips direction when f increases to the right, triple_point_finder only ever searches in the direction of increasing x and therefore requires f to be decreasing at x₀. A caller that cannot guarantee that — a line search whose direction came from a stale or regularized Jacobian, say — must check the anchor itself (see check_anchor).

When no triple can be found the function returns a Symbol rather than raising: a line search must be able to report an unbracketable merit rather than abort the enclosing solve. The two failures mean opposite things and are therefore distinguished, because a caller that conflates them reports a descending merit as stagnation:

  • :flat — the rise at the first probe is within the round-off resolution of f(x₀) (armijo_tolerance), so f does not resolve a decrease here at all. No line search can improve on this point (LINESEARCH_FLOOR).
  • :unbracketable — there is a decrease, but it cannot be bracketed: either nmax doublings never reached a turning point, or f rose at every probe down to the smallest δ tried. This is a genuine failure to report (LINESEARCH_EXHAUSTED), not a floor.

A third status, :capped, is not a failure at all: the search reached the ceiling αmax (see linesearch_αmax) while f was still falling, so the turning point lies beyond the largest step the caller allows and that ceiling is the answer. It is reachable only through _triple_point_core, which is where the αmax argument lives; triple_point_finder itself takes no ceiling and therefore never reports it.

Implementation

For δ we take DEFAULT_BRACKETING_s as default. For nmax we take DEFAULT_BRACKETING_nmax as default.

Examples

julia> f(x) = x ^ 2
f (generic function with 1 method)

julia> x = -1.
-1.0

julia> a, b, c = round10.(triple_point_finder(f, x))
(-0.37, 0.27, 1.55)

julia> round10.((f(a), f(b), f(c)))
(0.1369, 0.0729, 2.4025)

Extended help

The algorithm is taken from [4, Chapter 11.2.1].

source
SimpleSolvers.with_configMethod
with_config(ls, config)

Return a Linesearch with the LinesearchProblem and the LinesearchMethod of ls, but with the Options config.

This is how a NonlinearSolver makes its line search share its options. A Linesearch built by Linesearch(problem, method) carries an Options of its own, constructed from nothing but defaults — so verbosity and linesearch_max_iterations would be configured twice, and verbosity = 0 on the solver would not silence the line search.

Linesearch is an immutable three-field wrapper, so rebuilding it is cheap: the problem (and hence its closures and scratch buffers) and the method are shared, not copied.

The Options element type is pinned to the Linesearch element type, so a mismatched config raises a MethodError rather than silently producing a broken object — the same guarantee the three-argument Linesearch constructor gives.

source
SimpleSolvers.zero_likeMethod
zero_like(A)

A zeroed matrix with the same storage as A — and, for a sparse matrix, the same pattern.

zero(::SparseMatrixCSC) returns a matrix with no stored entries at all, which for a sparse Jacobian buffer is not a zeroed Jacobian but an empty one: the pattern is structural information the caller's DF! assembles into, and a DF! that writes only where the pattern says it may would find nowhere to write. Used for the line search's private Jacobian buffer, which has to be interchangeable with the solver's own.

source