SimpleSolvers
SimpleSolvers.ARMIJO_τ_DEMAND_FRACTIONSimpleSolvers.BACKTRACKING_GROW_MINSimpleSolvers.BACKTRACKING_SHRINK_MINSimpleSolvers.DEFAULT_ARMIJO_pSimpleSolvers.DEFAULT_ARMIJO_τ_ULPSSimpleSolvers.DEFAULT_BACKTRACKING_NEXPANDSimpleSolvers.DEFAULT_BACKTRACKING_qSimpleSolvers.DEFAULT_BRACKETING_kSimpleSolvers.DEFAULT_BRACKETING_nmaxSimpleSolvers.DEFAULT_BRACKETING_sSimpleSolvers.DEFAULT_ITERATIONS_QUASI_NEWTON_SOLVERSimpleSolvers.DEFAULT_LINESEARCH_αmaxSimpleSolvers.DEFAULT_PICARD_BACKTRACKING_pSimpleSolvers.DEFAULT_WOLFE_c₁SimpleSolvers.DEFAULT_WOLFE_c₂SimpleSolvers.DEFAULT_WOLFE_αmaxSimpleSolvers.DEFAULT_s_REDUCTIONSimpleSolvers.DOGLEG_Δ_EXPANDSimpleSolvers.DOGLEG_Δ_INITIALSimpleSolvers.DOGLEG_Δ_MAXSimpleSolvers.DOGLEG_Δ_SHRINKSimpleSolvers.DOGLEG_ηSimpleSolvers.DOGLEG_ρ_HIGHSimpleSolvers.DOGLEG_ρ_LOWSimpleSolvers.F_STALL_FACTORSimpleSolvers.F_STALL_REPORT_MINIMUMSimpleSolvers.F_STALL_WINDOWSimpleSolvers.HAS_PREALLOCATED_GETRFSimpleSolvers.MAX_STALLSSimpleSolvers.NLINESEARCH_OUTCOMESSimpleSolvers.N_STATIC_THRESHOLDSimpleSolvers.WARNING_COUNTSSimpleSolvers.WARNING_LOCKSimpleSolvers.AbstractLinearProblemSimpleSolvers.AbstractNonlinearSolverCacheSimpleSolvers.BacktrackingSimpleSolvers.BacktrackingConditionSimpleSolvers.BierlaireQuadraticSimpleSolvers.BisectionSimpleSolvers.BisectionOutcomeSimpleSolvers.BracketMinimumCriterionSimpleSolvers.BracketRootCriterionSimpleSolvers.BracketingCriterionSimpleSolvers.CurvatureConditionSimpleSolvers.DogLegSimpleSolvers.DogLegCacheSimpleSolvers.DogLegSolverSimpleSolvers.DogLegSolverSimpleSolvers.GradientSimpleSolvers.GradientAutodiffSimpleSolvers.GradientFiniteDifferencesSimpleSolvers.GradientFunctionSimpleSolvers.HessianSimpleSolvers.HessianAutodiffSimpleSolvers.HessianFunctionSimpleSolvers.JacobianSimpleSolvers.JacobianSimpleSolvers.JacobianAutodiffSimpleSolvers.JacobianFiniteDifferencesSimpleSolvers.JacobianFunctionSimpleSolvers.LUSimpleSolvers.LUSolverCacheSimpleSolvers.LapackLUSimpleSolvers.LinearProblemSimpleSolvers.LinearSolverSimpleSolvers.LinearSolverCacheSimpleSolvers.LinearSolverMethodSimpleSolvers.LinesearchSimpleSolvers.LinesearchMethodSimpleSolvers.LinesearchOutcomeSimpleSolvers.LinesearchProblemSimpleSolvers.LinesearchStatusSimpleSolvers.LinesearchStatusSimpleSolvers.NewtonSimpleSolvers.NewtonSolverSimpleSolvers.NewtonSolverSimpleSolvers.NewtonSolverSimpleSolvers.NoLinearProblemSimpleSolvers.NonlinearProblemSimpleSolvers.NonlinearSolverSimpleSolvers.NonlinearSolverCacheSimpleSolvers.NonlinearSolverMethodSimpleSolvers.NonlinearSolverStateSimpleSolvers.NonlinearSolverStatusSimpleSolvers.OptionsSimpleSolvers.PicardSimpleSolvers.PicardSolverSimpleSolvers.PicardSolverSimpleSolvers.PivotedLUCacheSimpleSolvers.PivotedLUMethodSimpleSolvers.QuadraticSimpleSolvers.RecursiveLUSimpleSolvers.SparseDirectMethodSimpleSolvers.SparseFactorizationCacheSimpleSolvers.SparspakLUSimpleSolvers.StaticSimpleSolvers.StrongWolfeSimpleSolvers.SufficientDecreaseConditionSimpleSolvers.UmfpackLUGeometricBase.update!GeometricBase.update!LinearAlgebra.ldiv!SimpleSolvers.QuasiNewtonSimpleSolvers._bisect_for_minimumSimpleSolvers._bracket_coreSimpleSolvers._bracket_minimum_coreSimpleSolvers._bracket_minimum_with_fixed_point_coreSimpleSolvers._getrf!SimpleSolvers._pivoted_lu_cacheSimpleSolvers._solve_core!SimpleSolvers._sparse_ldiv!SimpleSolvers._staticSimpleSolvers.absolute_toleranceSimpleSolvers.add_to_diagonal!SimpleSolvers.alloc_gSimpleSolvers.alloc_hSimpleSolvers.alloc_rhsSimpleSolvers.alloc_xSimpleSolvers.armijo_toleranceSimpleSolvers.armijo_ulpsSimpleSolvers.assess_convergenceSimpleSolvers.backtracking_extrapolationSimpleSolvers.backtracking_interpolationSimpleSolvers.backtracking_αminSimpleSolvers.bisectionSimpleSolvers.bracketSimpleSolvers.bracket_minimumSimpleSolvers.bracket_minimum_with_fixed_pointSimpleSolvers.bracket_rootSimpleSolvers.cacheSimpleSolvers.capped_statusSimpleSolvers.change_precisionSimpleSolvers.check_anchorSimpleSolvers.check_gradientSimpleSolvers.check_hessianSimpleSolvers.check_jacobianSimpleSolvers.checkfactorizedSimpleSolvers.checkfactorizedSimpleSolvers.checkjacobianprototypeSimpleSolvers.checkpatternSimpleSolvers.checksparseSimpleSolvers.clear!SimpleSolvers.compute_new_iterate!SimpleSolvers.convert_αmaxSimpleSolvers.copy_matrix!SimpleSolvers.curvature_diagnosticSimpleSolvers.default_linear_solver_methodSimpleSolvers.default_linesearch_αmaxSimpleSolvers.default_precisionSimpleSolvers.default_toleranceSimpleSolvers.default_ϵSimpleSolvers.directionSimpleSolvers.direction!SimpleSolvers.directions!SimpleSolvers.direction₁SimpleSolvers.direction₂SimpleSolvers.dogleg_direction!SimpleSolvers.dominant_linesearch_outcomeSimpleSolvers.f_stall_factorSimpleSolvers.f_stall_windowSimpleSolvers.factorizationSimpleSolvers.factorize!SimpleSolvers.factorize!SimpleSolvers.factorize!SimpleSolvers.fill_nan!SimpleSolvers.flag_stall!SimpleSolvers.havenonfiniteSimpleSolvers.increase_iteration_number!SimpleSolvers.initial_residualSimpleSolvers.initialize!SimpleSolvers.initialize!SimpleSolvers.isbenignSimpleSolvers.isconvergedSimpleSolvers.isfloorSimpleSolvers.isnotprogressingSimpleSolvers.isstalledSimpleSolvers.issufficientSimpleSolvers.iterate_settledSimpleSolvers.iteration_numberSimpleSolvers.iterations_since_progressSimpleSolvers.jacobianSimpleSolvers.jacobianmatrixSimpleSolvers.linearsolverSimpleSolvers.linesearch_failuresSimpleSolvers.linesearch_failuresSimpleSolvers.linesearch_indexSimpleSolvers.linesearch_iterationsSimpleSolvers.linesearch_max_iterationsSimpleSolvers.linesearch_outcomesSimpleSolvers.linesearch_outcomesSimpleSolvers.linesearch_problemSimpleSolvers.linesearch_problemSimpleSolvers.linesearch_reasonSimpleSolvers.linesearch_warningsSimpleSolvers.linesearch_αmaxSimpleSolvers.lucache_eltypeSimpleSolvers.lucache_matrixSimpleSolvers.max_stallsSimpleSolvers.maybe_refactorize!SimpleSolvers.meets_stopping_criteriaSimpleSolvers.methodSimpleSolvers.method_αmaxSimpleSolvers.minimum_decrease_thresholdSimpleSolvers.nan_recovery!SimpleSolvers.needs_refreshSimpleSolvers.no_progressSimpleSolvers.nonlinear_solver_warningsSimpleSolvers.nonlinearproblemSimpleSolvers.outcomeSimpleSolvers.pivot_indexSimpleSolvers.print_jacobianSimpleSolvers.print_statusSimpleSolvers.record_iteration!SimpleSolvers.record_linesearch!SimpleSolvers.record_progress!SimpleSolvers.record_stall!SimpleSolvers.report_linesearch_statusSimpleSolvers.reset_warning_counts!SimpleSolvers.residual_smallSimpleSolvers.residualsSimpleSolvers.resolve_jacobianSimpleSolvers.resolve_linear_solver_methodSimpleSolvers.rhsSimpleSolvers.shift_χ_to_avoid_stallingSimpleSolvers.should_report!SimpleSolvers.singular_indexSimpleSolvers.singular_indexSimpleSolvers.singular_indexSimpleSolvers.singular_indexSimpleSolvers.solveSimpleSolvers.solveSimpleSolvers.solveSimpleSolvers.solveSimpleSolvers.solveSimpleSolvers.solveSimpleSolvers.solve!SimpleSolvers.solve!SimpleSolvers.solve!SimpleSolvers.solve!SimpleSolvers.solve!SimpleSolvers.solve!SimpleSolvers.solve_with_statusSimpleSolvers.solve_with_statusSimpleSolvers.solve_with_statusSimpleSolvers.solve_with_statusSimpleSolvers.solve_with_statusSimpleSolvers.solve_with_statusSimpleSolvers.solve_with_status!SimpleSolvers.solver_step!SimpleSolvers.solver_step!SimpleSolvers.spent_without_progressSimpleSolvers.stall_numberSimpleSolvers.stalled_stepSimpleSolvers.statusSimpleSolvers.steplengthSimpleSolvers.trialsSimpleSolvers.triple_point_finderSimpleSolvers.trust_radiusSimpleSolvers.trust_radius!SimpleSolvers.value!SimpleSolvers.with_configSimpleSolvers.zero_like
SimpleSolvers.ARMIJO_τ_DEMAND_FRACTION — Constant
const ARMIJO_τ_DEMAND_FRACTIONThe largest fraction of the demanded decrease that the round-off resolution $\tau$ is allowed to amount to, used by armijo_ulps to cap $\tau$ in low precision. Its value is 0.01, i.e. $\tau$ may distort the SufficientDecreaseCondition by at most one percent.
SimpleSolvers.BACKTRACKING_GROW_MIN — Constant
const BACKTRACKING_GROW_MINLower 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.
SimpleSolvers.BACKTRACKING_SHRINK_MIN — Constant
const BACKTRACKING_SHRINK_MINLower 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.
SimpleSolvers.DEFAULT_ARMIJO_p — Constant
const DEFAULT_ARMIJO_pConstant used in Backtracking. Its value is 0.5
This is the default for the constant $p$ by which α is decreased if the SufficientDecreaseCondition and the CurvatureCondition are not satisfied.
SimpleSolvers.DEFAULT_ARMIJO_τ_ULPS — Constant
const DEFAULT_ARMIJO_τ_ULPSThe 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:
- it slackens the
SufficientDecreaseConditioninsideBacktrackingto $\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)$; - it fixes the smallest informative trial step $\alpha_\mathrm{min}$ below which the condition could only be decided by rounding (see
backtracking_αmin); - every
LinesearchMethoduses it to decide whether an accepted step's decrease was genuine (LINESEARCH_DECREASED) or within the noise (LINESEARCH_FLOOR, seeLinesearchOutcome).
Set τ_ulps = 0 in Backtracking to recover the exact condition for (1) and (2).
SimpleSolvers.DEFAULT_BACKTRACKING_NEXPAND — Constant
const DEFAULT_BACKTRACKING_NEXPANDThe default cap on the number of expansion trials of Backtracking; each one costs a merit evaluation. Its value is 3.
SimpleSolvers.DEFAULT_BACKTRACKING_q — Constant
const DEFAULT_BACKTRACKING_qThe default upper bound on the factor by which the expansion phase of Backtracking lengthens the step in one round, i.e. the counterpart of $p$ on the growing side. Its value is 10.0.
SimpleSolvers.DEFAULT_BRACKETING_k — Constant
const DEFAULT_BRACKETING_kGives the default ratio by which the bracket is increased if bracketing was not successful. See bracket_minimum.
SimpleSolvers.DEFAULT_BRACKETING_nmax — Constant
Default constant. Number of maximum iterations for bracket_minimum, bracket_minimum_with_fixed_point and bracket_root.
SimpleSolvers.DEFAULT_BRACKETING_s — Constant
const DEFAULT_BRACKETING_sGives the default initial width of the interval (the bracket). Used for bracket_minimum, bracket_minimum_with_fixed_point and bracket_root.
SimpleSolvers.DEFAULT_ITERATIONS_QUASI_NEWTON_SOLVER — Constant
The default number of iterations before the Jacobian is refactored when constructing a quasi-Newton method via QuasiNewton.
SimpleSolvers.DEFAULT_LINESEARCH_αmax — Constant
const DEFAULT_LINESEARCH_αmaxThe 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.
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.
SimpleSolvers.DEFAULT_PICARD_BACKTRACKING_p — Constant
Backtracking shrink factor used by the PicardSolver residual safeguard.
SimpleSolvers.DEFAULT_WOLFE_c₁ — Constant
const DEFAULT_WOLFE_c₁A constant $c_1$ that is used in the SufficientDecreaseCondition (the Armijo condition):
\[f(\alpha) \leq f(\alpha_0) + c_1 \alpha f'(\alpha_0).\]
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.
SimpleSolvers.DEFAULT_WOLFE_αmax — Constant
const DEFAULT_WOLFE_αmaxDefault 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.
SimpleSolvers.DEFAULT_s_REDUCTION — Constant
A factor by which s is reduced in each bracketing iteration (see bracket_minimum_with_fixed_point).
SimpleSolvers.DOGLEG_Δ_EXPAND — Constant
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.
SimpleSolvers.DOGLEG_Δ_INITIAL — Constant
Default initial trust-region radius for the DogLegSolver; the default of the dogleg_radius_initial field of Options, which the solver actually reads.
SimpleSolvers.DOGLEG_Δ_MAX — Constant
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.
SimpleSolvers.DOGLEG_Δ_SHRINK — Constant
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.
SimpleSolvers.DOGLEG_η — Constant
Minimum ρ (actual/predicted reduction) for a step to be accepted ($\eta$ in [1, Alg. 4.1]).
SimpleSolvers.DOGLEG_ρ_HIGH — Constant
Upper ρ threshold above which the trust-region radius may be expanded.
SimpleSolvers.DOGLEG_ρ_LOW — Constant
Lower ρ threshold below which the trust-region radius is shrunk.
SimpleSolvers.F_STALL_FACTOR — Constant
const F_STALL_FACTORThe 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.
SimpleSolvers.F_STALL_REPORT_MINIMUM — Constant
const F_STALL_REPORT_MINIMUMThe 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).
SimpleSolvers.F_STALL_WINDOW — Constant
const F_STALL_WINDOWThe 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.
SimpleSolvers.HAS_PREALLOCATED_GETRF — Constant
HAS_PREALLOCATED_GETRFWhether 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.
SimpleSolvers.MAX_STALLS — Constant
const MAX_STALLSThe 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.
SimpleSolvers.NLINESEARCH_OUTCOMES — Constant
const NLINESEARCH_OUTCOMESThe number of LinesearchOutcomes, i.e. the length of the tally a NonlinearSolverState keeps of the outcomes its line search reported (see record_linesearch!). A const rather than length(instances(LinesearchOutcome)) at each use, because it is a type parameter of that tally's MVector and so has to be known to the compiler.
SimpleSolvers.N_STATIC_THRESHOLD — Constant
Threshold for the maximum size a static matrix should have. See _static.
SimpleSolvers.WARNING_COUNTS — Constant
const WARNING_COUNTSHow often each diagnosis has been reported so far, keyed by the Symbol passed to should_report!. Process-global, and guarded by WARNING_LOCK.
The number of keys is bounded by the number of message sites, so this does not grow without limit. It is reset by reset_warning_counts!.
SimpleSolvers.WARNING_LOCK — Constant
Guards WARNING_COUNTS; see should_report!.
SimpleSolvers.AbstractLinearProblem — Type
Encompasses the NoLinearProblem and the LinearProblem. Subtyped from AbstractProblem, coming from GeometricBase.
SimpleSolvers.AbstractNonlinearSolverCache — Type
AbstractNonlinearSolverCacheAn abstract type that comprises e.g. the NonlinearSolverCache and the DogLegCache.
SimpleSolvers.Backtracking — Type
Backtracking <: LinesearchMethodKeys
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 theSufficientDecreaseCondition(Armijo condition). Also seeDEFAULT_WOLFE_c₁.c₂=0.9: the constant on whose basis theCurvatureConditionis tested. We should have $c_2\in(c_1, 1).$ The closer this constant is to 1, the easier it is to satisfy theCurvatureCondition.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 aBacktrackingis 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 whenexpandis set. SeeDEFAULT_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 thelinesearch_max_iterationsofOptionsrather than beside it: whichever of the two is smaller applies, so the whole search still spends at mostlinesearch_max_iterationsmerit evaluations. Only used whenexpandis set. SeeDEFAULT_BACKTRACKING_NEXPAND.τ_ulps=armijo_ulps(T, c₁)(4 inFloat64andFloat32, less inFloat16): the round-off resolution of the merit, in units in the last place of $\varphi(0)$. It slackens theSufficientDecreaseCondition(never past $\varphi(0)$), fixes $\alpha_\mathrm{min}$, and separates a genuine decrease from one within the noise. A value larger thanarmijo_ulps(T, c₁)is capped to it, since above that $\tau$ would swamp the decrease the condition demands. SeeDEFAULT_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:
- the
SufficientDecreaseConditionis satisfied — the step is accepted, and reported as a genuine decrease only if $\varphi(\alpha) \leq \varphi_0 - \tau$; - 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;
- $\alpha \leq \alpha_\mathrm{min}$ — a smaller step could only be judged by rounding;
- the
linesearch_max_iterationsbudget ofOptionsis 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.
SimpleSolvers.BacktrackingCondition — Type
BacktrackingConditionAbstract type comprising the conditions that are used for checking step sizes for the backtracking line search (see Backtracking). This comprises the SufficientDecreaseCondition and the CurvatureCondition.
SimpleSolvers.BierlaireQuadratic — Type
BierlaireQuadratic <: LinesearchMethodAlgorithm 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 defaultDEFAULT_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; seelinesearch_αmax, which is also how a caller imposes a smaller ceiling of its own.
SimpleSolvers.Bisection — Type
Bisection <: LinesearchMethodSee bisection for the implementation of the algorithm.
Keywords
αmax: the largest step the bracketing will try, by defaultDEFAULT_LINESEARCH_αmax. Seelinesearch_αmax, which is also how a caller imposes a smaller ceiling of its own.
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 tobisectionwith 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_sand1so 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.
SimpleSolvers.BisectionOutcome — Type
BisectionOutcomeWhy _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_abstolor because the bracket collapsed tox_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: thelinesearch_max_iterationsbudget ofOptionswas spent with the interval still straddling a sign change. UnlikeBISECTION_NOBRACKETthere 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.
SimpleSolvers.BracketMinimumCriterion — Type
BracketMinimumCriterion <: BracketingCriterionThe criterion used for bracket_minimum. It checks whether $y(c)$ is greater than or equal to $y(b)$ (i.e. checks whether we are passed the minimum). Compare this with BracketRootCriterion.
Functor
bc = BracketMinimumCriterion()
yc = .1
yb = .2
bc(yb, yc)
# output
falseSimpleSolvers.BracketRootCriterion — Type
BracketRootCriterion <: BracketingCriterionThe criterion used for bracket_root. It checks whether there is a sign change between $b$ and $c$ (i.e. checks whether there is a root between those two points). Compare this with BracketMinimumCriterion.
Functor
bc = BracketRootCriterion()
yc = .1
yb = -.2
bc(yb, yc)
# output
trueSimpleSolvers.BracketingCriterion — Type
BracketingCriterionAbstract type for the criteria used while bracketing. It determines when a bracket has been found. The two concrete subtypes are BracketMinimumCriterion (used by bracket_minimum) and BracketRootCriterion (used by bracket_root).
SimpleSolvers.CurvatureCondition — Type
CurvatureCondition <: BacktrackingConditionThe 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.
SimpleSolvers.DogLeg — Type
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.
SimpleSolvers.DogLegCache — Type
DogLegCacheLike NonlinearSolverCache but storing two directions (callable with direction₁ and direction₂).
SimpleSolvers.DogLegSolver — Type
DogLegSolverThe NonlinearSolver for the DogLeg method.
Note that the DogLeg solver_step! is a trust-region method: it chooses the step length via the trust-region radius, not a line search. Unlike the NewtonSolver — but like the PicardSolver — no linesearch keyword is accepted (passing one is an error rather than being silently ignored).
SimpleSolvers.DogLegSolver — Method
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
linear_solver_methodjacobian: seeresolve_jacobian,jacobian_prototype: see theNewtonSolvercounterpart. Note thatSparspakLUreports singularity too late for theDogLegfallback; see its docstring,refactorizeoptions_kwargs: seeOptions.
SimpleSolvers.Gradient — Type
GradientAbstract type. structs that are derived from this need an associated functor that computes the gradient of a function (in-place).
Examples
Examples include:
SimpleSolvers.GradientAutodiff — Type
GradientAutodiff <: GradientA struct that realizes Gradient by using ForwardDiff.
Keys
The struct stores:
F: a function that has to be differentiated.∇config: result of applyingForwardDiff.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)SimpleSolvers.GradientFiniteDifferences — Type
GradientFiniteDifferences <: GradientA 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 thexvector.
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ϵⱼ)
endSimpleSolvers.GradientFunction — Type
GradientFunction <: GradientA 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)SimpleSolvers.Hessian — Type
HessianAbstract 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) endExamples
Examples include:
SimpleSolvers.HessianAutodiff — Type
HessianAutodiff <: HessianA struct that realizes Hessian by using ForwardDiff.
Keys
The struct stores:
F: a function that has to be differentiated.Hconfig: result of applyingForwardDiff.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)SimpleSolvers.HessianFunction — Type
HessianFunction <: HessianA 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)SimpleSolvers.Jacobian — Type
JacobianAbstract type. structs that are derived from this need an associated functor that computes the Jacobian of a function (in-place).
Implementation
When a custom Jacobian is implemented, a functor is needed:
function (j::Jacobian)(g::AbstractMatrix, x::AbstractVector) endExamples
Examples include:
SimpleSolvers.Jacobian — Method
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:
mode = :autodiff(default) builds aJacobianAutodiff(ForwardDiff).mode = :finitedifferencesbuilds aJacobianFiniteDifferences; any remaining keyword arguments (e.g.ϵ) are forwarded to it.
The convenience forms Jacobian{T}(F, n), Jacobian(F, x) and Jacobian(F, x, y) forward here.
SimpleSolvers.JacobianAutodiff — Type
JacobianAutodiff <: JacobianA struct that realizes Jacobian by using ForwardDiff.
Keys
The struct stores:
F: a function that has to be differentiated.Jconfig: result of applyingForwardDiff.JacobianConfig.ty: vector that is used for evaluatingForwardDiff.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)SimpleSolvers.JacobianFiniteDifferences — Type
JacobianFiniteDifferences <: JacobianA 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 thexvector.
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
endSimpleSolvers.JacobianFunction — Type
JacobianFunction <: JacobianA 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.0SimpleSolvers.LU — Type
struct LU <: DirectMethodA 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
trueNote that role of LinearProblem here.
SimpleSolvers.LUSolverCache — Type
LUSolverCache <: LinearSolverCacheThe cache for the LU solver.
Keys
A: the factorized matrixA,pivots: a vector of pivots used during factorization,perms: a vector of permutations used during factorization,info: stores an index regarding pivoting.
SimpleSolvers.LapackLU — Type
struct LapackLU <: PivotedLUMethodA 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:
| n | LU(static=false) | LapackLU | RecursiveLU |
|---|---|---|---|
| 12 | 0.24 | 0.63 | 0.14 |
| 64 | 22.9 | 10.8 | 6.65 |
| 128 | 182 | 59.6 | 42.5 |
| 384 | 6526 | 531 | 961 |
| 768 | 51109 | 1613 | 7349 |
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
trueSimpleSolvers.LinearProblem — Type
LinearProblemA LinearProblem describes $Ax = y$, where we want to solve for $x$.
Keys
Ay
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])SimpleSolvers.LinearSolver — Type
LinearSolver <: AbstractSolverA 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)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!.
SimpleSolvers.LinearSolverCache — Type
LinearSolverCacheAn abstract type that summarizes all the caches used for LinearSolvers. See e.g. LUSolverCache.
SimpleSolvers.LinearSolverMethod — Type
LinearSolverMethod <: SolverMethodSummarizes 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.
SimpleSolvers.Linesearch — Type
LinesearchA struct that stores a LinesearchProblem, LinesearchMethod and Options.
Keys
problem::LinesearchProblemmethod::LinesearchMethodconfig::Options
Constructors
The following constructors can be used:
Linesearch{T}(problem, method, config)
Linesearch(problem, method=Static(); kwargs...)
Linesearch(problem, method, config::Options)SimpleSolvers.LinesearchMethod — Type
LinesearchMethod{T} <: SolverMethodExamples 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:
- 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(seebracket_minimum,triple_point_finder) and the method maps that onto aLinesearchOutcome. The one deliberate exception is a caller error rather than a situation arising from the merit: aparams.αmaxthat is not a usable ceiling raises anArgumentErrorbefore any evaluation — seelinesearch_αmax. - 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. - It reports through
linesearch_warningsonly — one message site and one verbosity policy for all methods (genuine failure atverbosity ≥ 1; the benign round-off-floor and stationary outcomes at≥ 2). And it reports there only when the user called it: a program callssolve_with_status, acts on theLinesearchStatusand sees no messages at all. ANonlinearSolveris such a program — seerecord_linesearch!. This is structural rather than a convention a method has to keep: a method implementssolve_with_status, andsolveis derived from it once, for all methods, as that call plus the report. - A non-finite or ascending anchor is reported, not assumed away — see
check_anchor. - 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.
- It returns $\alpha \leq \alpha_\mathrm{max}$ — see
linesearch_αmaxfor 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$-relative —
Backtracking,StrongWolfeand triviallyStatic: "given the trial step $\alpha$, return a step satisfying a decrease condition". The result depends on the input $\alpha$.Backtrackingshrinks it only, and therefore returns it unchanged whenever it is acceptable, unless itsexpandkey is set;StrongWolfebrackets in both directions. - Minimising, $\alpha$-independent —
Bisection,QuadraticandBierlaireQuadratic: "approximate the minimiser of $\varphi$ along the direction". The input $\alpha$ only seeds the bracketing (see issue #164), and no Wolfe condition is checked.
SimpleSolvers.LinesearchOutcome — Type
LinesearchOutcomeWhy 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:BacktrackingandStrongWolfeadditionally 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 atverbosity ≥ 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 (seestalled_stepandOptions).LINESEARCH_EXHAUSTED: no acceptable step although the merit does vary by more than $\tau$. Either $\varphi'(0)$ is inconsistent with $\varphi$ (a stale or regularizedJacobian, an inexact linear solve, a non-smooth merit), or thelinesearch_max_iterationsbudget ofOptionswas 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.
SimpleSolvers.LinesearchProblem — Type
LinesearchProblem <: AbstractProblemIn practice LinesearchProblems are allocated by calling linesearch_problem.
Constructors
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)SimpleSolvers.LinesearchStatus — Type
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 valuesolvereturns),outcome::LinesearchOutcome,trials: what the search cost, as the number of times it evaluated the problem — not thelinesearch_max_iterationsbudget. That is the merit for every method exceptBisection, which drives on the derivative it bisects and brackets on the merit, so its count is of both. ForBacktrackingandStrongWolfeit 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 (seearmijo_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 (seebacktracking_αmin). This is a shrinking-ladder quantity and is thereforezero— meaning "not applicable" — for the minimising searches (Bisection,Quadratic,BierlaireQuadratic) and forStrongWolfe, which bracket rather than shrink.
SimpleSolvers.LinesearchStatus — Method
LinesearchStatus(α, outcome=LINESEARCH_UNKNOWN)Construct a LinesearchStatus that carries only the step length and the LinesearchOutcome; the remaining diagnostics are filled with NaN/zero. For a method that does not measure them — Static, or a third-party method whose solve_with_status has nothing more to say.
SimpleSolvers.Newton — Type
Newton(refactorize=1)The Newton (and quasi-Newton) nonlinear solver method.
Constructors
Newton()
# output
Newton(1)QuasiNewton()
# output
Newton(5)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).
SimpleSolvers.NewtonSolver — Type
NewtonSolverA 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
trueKeywords
linear_solver_method: the method used to build the linear solver (seeLinearSolver) that computes the direction of the solver step (seesolver_step!),DF!: an in-place function computing the Jacobian,linesearch::Linesearchjacobian::Jacobianjacobian_prototype: the matrix whose storage and, if sparse, whose sparsity pattern are adopted by the Jacobian, theLinearProblemand theLinearSolvercache. It is copied, so the caller's matrix is left alone. ASparseMatrixCSChere is what runs a sparse Jacobian through the solver, and it requiresDF!(seecheckjacobianprototype) and a pattern that includes the diagonal ifregularization_factoris non-zero. It also selects the defaultlinear_solver_method; seedefault_linear_solver_method,refactorize::Int: determines after how many steps the Jacobian is re-evaluated and refactored (seefactorize!).refactorize > 1gives a quasi-Newton method (seeQuasiNewton),options_kwargs: seeOptions
SimpleSolvers.NewtonSolver — Method
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.
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_methodlinesearchjacobianjacobian_prototyperefactorizeoptions_kwargs: seeOptions
Examples
F(y, x, params) = y .= sin.(x) .^ 2
x = ones(3)
nlp = NonlinearProblem(F, x)
NewtonSolver(x, nlp) isa NewtonSolver
# output
trueSimpleSolvers.NewtonSolver — Method
NewtonSolver(x, F, y)Keywords
linear_solver_methodDF!linesearchjacobianjacobian_prototyperefactorizeoptions_kwargs: seeOptions
The Callable F (and the optional DF!) are wrapped in a NonlinearProblem, so this is NewtonSolver(::AbstractVector{T}, ::NonlinearProblem, ::AbstractVector{T}) where {T} with the problem built for the caller.
SimpleSolvers.NoLinearProblem — Type
A dummy linear system used for the fixed point iterator (Picard).
SimpleSolvers.NonlinearProblem — Type
NonlinearProblemA 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
FJ::Union{Callable, Missing}: accessed by callingjacobian.
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)SimpleSolvers.NonlinearSolver — Type
NonlinearSolverA struct that comprises Newton solvers (see Newton), the Picard solver (also known as fixed-point iteration; see Picard) and the Dogleg solver (see DogLeg).
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
nonlinearproblem::NonlinearProblem: the system that has to be solved. This can be accessed by callingnonlinearproblem,linearproblem::LinearProblem,jacobian::Jacobian: the Jacobian is used to compute the direction in the solver step (seesolver_step!). This can be accessed by callingjacobian,linearsolver::LinearSolver: the linear solver is used to compute the direction of the solver step (seesolver_step!). This can be accessed by callinglinearsolver,linesearch::Linesearchmethod::NonlinearSolverMethod: the solver method (e.g.Newton),cache::NonlinearSolverCacheconfig::Options
SimpleSolvers.NonlinearSolverCache — Type
NonlinearSolverCache <: AbstractNonlinearSolverCacheDerived from AbstractNonlinearSolverCache. Used in NonlinearSolver.
Keys
x: the next iterate (or guess thereof),Δx: search direction. This is updated when callingsolver_step!via theLinearSolverstored in theNewtonSolver,rhs: the right-hand-side (this can be accessed by callingrhs),y: the problem evaluated atx,j::AbstractMatrix: the Jacobian evaluated atx. Note that this is not of typeJacobian!
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.
SimpleSolvers.NonlinearSolverMethod — Type
NonlinearSolverMethod <: SolverMethodA 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.
SimpleSolvers.NonlinearSolverState — Type
NonlinearSolverState <: AbstractSolverStateThe NonlinearSolverState to be used together with a NonlinearSolver.
Note the difference to the NonlinearSolverCache and the NonlinearSolverStatus.
Examples
julia> state = NonlinearSolverState(zeros(3))
NonlinearSolverState{Float64, Vector{Float64}, Vector{Float64}}(0, [NaN, NaN, NaN], [NaN, NaN, NaN], [NaN, NaN, NaN], [NaN, NaN, NaN], NaN, 0, false, NaN, 0, [0, 0, 0, 0, 0, 0])SimpleSolvers.NonlinearSolverStatus — Type
NonlinearSolverStatusStores absolute and successive residuals for x and f. It is used as a diagnostic tool in NewtonSolver.
Compare this to the NonlinearSolverState and the NonlinearSolverCache.
Keys
iterations: number of iterationsstalls: number of consecutive stalled steps, seestalled_stepandisstalled,iterations_since_progress: iterations since the residual last dropped byf_stall_factor, seeiterations_since_progress,rxₛ: successive residual inx,rfₐ: absolute residual inf,rfₛ: successive residual inf,x_converged::Boolf_converged::Boolf_increased::Boolstalled::Bool: the last step stalled, seestalled_stepnot_progressing::Bool: the iteration is not getting anywhere, seeno_progressls_outcomes: how often the line search reported eachLinesearchOutcomeduring the solve, indexed bylinesearch_index; seelinesearch_outcomesandlinesearch_failures
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ₛ= NaNSimpleSolvers.Options — Type
OptionsExamples
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
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.
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 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.
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.
Also see meets_stopping_criteria.
SimpleSolvers.Picard — Type
Picard <: NonlinearSolverMethodSee PicardSolver.
SimpleSolvers.PicardSolver — Method
PicardSolver(x, F)Arguments
x: the initial guess for the solution.F: the nonlinear function to solve.y
Keywords
DF!: the Jacobian ofF,jacobian: the Jacobian ofF, defaults toJacobianAutodiff,options_kwargs: seeOptions.
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.0SimpleSolvers.PicardSolver — Method
PicardSolver(x, nlp::NonlinearProblem, y = zero(x))Build a PicardSolver 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.
Keywords
jacobian: seeresolve_jacobian,options_kwargs: seeOptions.
SimpleSolvers.PivotedLUCache — Type
PivotedLUCache <: LinearSolverCacheThe cache shared by every PivotedLUMethod — LapackLU 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, or0on success,factorized: whetherfactorize!has run at all, whichinfo == 0cannot 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.
SimpleSolvers.PivotedLUMethod — Type
PivotedLUMethod <: DirectMethodThe 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.
SimpleSolvers.Quadratic — Type
Quadratic <: LinesearchMethodQuadratic 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 isDEFAULT_BRACKETING_s.s_reduction:A constant that determines the factor by whichsis decreased in each new bracketing iteration.αmax: the largest step the bracketing will try, by defaultDEFAULT_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; seelinesearch_α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].
SimpleSolvers.RecursiveLU — Type
struct RecursiveLU <: PivotedLUMethodA 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:
| n | LU(static=false) | LapackLU (OpenBLAS) | RecursiveLU |
|---|---|---|---|
| 12 | 0.24 | 0.63 | 0.14 |
| 32 | 3.47 | 3.36 | 1.40 |
| 64 | 22.9 | 10.8 | 6.65 |
| 128 | 182 | 59.6 | 42.5 |
| 256 | 1912 | 169 | 287 |
| 384 | 6526 | 531 | 961 |
| 768 | 51109 | 1613 | 7349 |
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.
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.
SimpleSolvers.SparseDirectMethod — Type
SparseDirectMethod <: DirectMethodThe direct methods that factorize a sparse matrix, keeping the ordering and symbolic factorization across refactorizations: UmfpackLU and SparspakLU.
Unlike a PivotedLUMethod these need the sparsity pattern up front, at LinearSolver construction, and they refuse a dense matrix. See SparseFactorizationCache.
SimpleSolvers.SparseFactorizationCache — Type
SparseFactorizationCache <: LinearSolverCacheThe 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:0if the last factorization or solve found nothing wrong, non-zero otherwise; seesingular_index,factorized: whetherfactorize!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!.
SimpleSolvers.SparspakLU — Type
struct SparspakLU <: SparseDirectMethodA 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 type | SparspakLU | residual | UmfpackLU |
|---|---|---|---|
Float64 | works | 1.1e-16 | works |
Float32 | works | 1.2e-7 | unsupported |
ComplexF64 | works | 1.1e-16 | works |
BigFloat | works | 1.7e-77 | unsupported |
Rational{BigInt} | works | 0.0 — exact | unsupported |
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.
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
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_indexreturns0until a solve has actually failed, and is a flag rather than a pivot position even then.DogLegSolverreadssingular_indexbefore solving, to decide whether the Newton leg is available (seeSimpleSolvers.directions!). With this method that check cannot fire, so a singular Jacobian surfaces as aSingularExceptionout of the solve rather than as a fallback to the steepest-descent leg. UseUmfpackLUwithDogLegwhere the element type allows it.
SimpleSolvers.Static — Type
Static <: LinesearchMethodThe static method.
Keys
Keys include:
α: equivalent to a step size. The default is1.
Examples
Static()
# output
Static with α = 1.0.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.
SimpleSolvers.StrongWolfe — Type
StrongWolfe{T} <: LinesearchMethodA 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
c₁(defaultDEFAULT_WOLFE_c₁): the Armijo constant $c_1$.c₂(defaultDEFAULT_WOLFE_c₂): the curvature constant $c_2$. We require $c_1 < c_2 < 1$.αmax(defaultDEFAULT_WOLFE_αmax): the largest step the bracketing phase will try.
SimpleSolvers.SufficientDecreaseCondition — Type
SufficientDecreaseCondition <: BacktrackingConditionThe 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.
SimpleSolvers.UmfpackLU — Type
struct UmfpackLU <: SparseDirectMethodA 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:
| n | nnz | UmfpackLU factorize | ldiv! | dense LapackLU factorize |
|---|---|---|---|---|
| 64 | 320 | 13.0 | 0.68 | 11.3 |
| 128 | 640 | 26.3 | 1.28 | 59.5 |
| 384 | 1920 | 76.2 | 3.52 | 525 |
| 1024 | 5120 | 207 | 8.6 | 2525 |
| 4096 | 20480 | 961 | 39.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.
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.
GeometricBase.update! — Method
update!(ls::LinearProblem, A, b)Set the rhs vector to b and the matrix stored in the LinearProblem ls to A.
Calling update! doesn't solve the LinearProblem, you still have to call solve! in combination with a LinearSolver.
GeometricBase.update! — Method
update!(state, x, y)Update x̄, ȳ, 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).
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
Note that we need to call factorize! here after having allocated the LinearSolver.
SimpleSolvers.QuasiNewton — Function
QuasiNewton(refactorize=5)Convenience constructor for a Newton method whose Jacobian is only re-evaluated and refactored every refactorize iterations. Equivalent to Newton(refactorize) but with a quasi-Newton default (see DEFAULT_ITERATIONS_QUASI_NEWTON_SOLVER).
SimpleSolvers._bisect_for_minimum — Method
_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.
SimpleSolvers._bracket_core — Method
_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.
SimpleSolvers._bracket_minimum_core — Method
_bracket_minimum_core(f, x, s, k, nmax, αmax)bracket_minimum with the (lo, hi, n, status) return of _bracket_core, so that a caller can tell a bracket truncated at the ceiling αmax from one that found a turning point and can charge its cost n to the trials it reports. Private; see _bracket_core.
SimpleSolvers._bracket_minimum_with_fixed_point_core — Method
_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.
SimpleSolvers._getrf! — Function
_getrf!(method, cache)Compute the LU factors of cache.A in place and record the zero-pivot index in cache.info.
This is the only thing that differs between the PivotedLUMethods; everything else — the cache, ldiv!, singular_index and every solve! form — is shared. See PivotedLUCache.
SimpleSolvers._pivoted_lu_cache — Method
_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.
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.
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.
SimpleSolvers._static — Method
_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.
SimpleSolvers.absolute_tolerance — Method
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.0julia> absolute_tolerance(Float32)
0.0f0SimpleSolvers.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.
SimpleSolvers.alloc_g — Function
alloc_g(x)Allocate NaNs of the size of the gradient of f (with respect to x).
SimpleSolvers.alloc_h — Function
alloc_h(x)Allocate NaNs of the size of the Hessian of f (with respect to x).
SimpleSolvers.alloc_rhs — Method
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.
SimpleSolvers.alloc_x — Function
alloc_x(x)Allocate NaNs of the size of x.
SimpleSolvers.armijo_tolerance — Method
armijo_tolerance(φ₀, n)The absolute round-off resolution $\tau = n\cdot\mathrm{ulp}(\varphi_0)$ of a merit function, where n is a number of units in the last place — armijo_ulps for the default. See DEFAULT_ARMIJO_τ_ULPS for what it is used for and backtracking_αmin for the step length derived from it.
This lives here rather than next to Backtracking because triple_point_finder needs it too, and bracketing/ is included before linesearch/.
SimpleSolvers.armijo_ulps — Method
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)SimpleSolvers.assess_convergence — Method
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_small ⟺ rfₐ ≤ 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_suctolandresidual_small,f_converged: (rfₛ ≤ norm(value(state)) * config.f_suctolandresidual_small) orrfₐ ≤ 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.
SimpleSolvers.backtracking_extrapolation — Method
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.
SimpleSolvers.backtracking_interpolation — Method
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.
SimpleSolvers.backtracking_αmin — Method
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.
$\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.
SimpleSolvers.bisection — Method
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).
When calling bisection it first checks if $x_\mathrm{min} < x_\mathrm{max}$ and else flips the two entries.
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.
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.
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.
SimpleSolvers.bracket — Method
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.
SimpleSolvers.bracket_minimum — Method
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
f: the function to be bracketed,x: the starting point,s: by defaultDEFAULT_BRACKETING_s,k: by defaultDEFAULT_BRACKETING_k,nmax: by defaultDEFAULT_BRACKETING_nmax,αmax: how far to the right the search may probe, by defaultInf(seelinesearch_αmax). A bracket truncated by it is returned like any other, so a caller that has to tell the two apart uses_bracket_minimum_core.
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.
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.
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.
SimpleSolvers.bracket_minimum_with_fixed_point — Method
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.
SimpleSolvers.bracket_root — Method
bracket_root(f, x)Make a bracket for the function based on x (for root finding).
This is largely equivalent to bracket_minimum. See the end of that docstring for more information.
Here we use BracketRootCriterion instead of BracketMinimumCriterion.
SimpleSolvers.cache — Method
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)SimpleSolvers.capped_status — Method
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.
SimpleSolvers.change_precision — Method
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).
SimpleSolvers.check_anchor — Method
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 aNonlinearSolverthis 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.
SimpleSolvers.check_gradient — Method
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.0SimpleSolvers.check_hessian — Method
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).
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.0SimpleSolvers.check_jacobian — Method
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).
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.0SimpleSolvers.checkfactorized — Method
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.
SimpleSolvers.checkfactorized — Method
checkfactorized(lsolver::LinearSolver{T,<:SparseDirectMethod})Throw an ArgumentError if factorize! has not been called on lsolver yet.
The SparseDirectMethod counterpart of the PivotedLUMethod guard.
SimpleSolvers.checkjacobianprototype — Method
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.
SimpleSolvers.checkpattern — Method
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.
SimpleSolvers.checksparse — Method
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.
SimpleSolvers.clear! — Method
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].
SimpleSolvers.convert_αmax — Method
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.
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.
SimpleSolvers.curvature_diagnostic — Method
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.
SimpleSolvers.default_linear_solver_method — Method
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:
A | method |
|---|---|
dense, Float32/Float64/ComplexF32/ComplexF64 | LapackLU |
dense, anything else — BigFloat, Rational, … | LU |
sparse, Float64/ComplexF64 | UmfpackLU |
| sparse, anything else | none — 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.
SimpleSolvers.default_linesearch_αmax — Method
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)SimpleSolvers.default_precision — Method
default_precision(T)Compute the default precision used for e.g. BierlaireQuadratic.
Compare this to the default_tolerance used in Options.
Examples
julia> default_precision(Float64)
1.7763568394002505e-15julia> default_precision(Float32)
9.536743f-7julia> default_precision(Float16)
Float16(0.007812)SimpleSolvers.default_tolerance — Method
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-16julia> default_tolerance(Float32)
2.3841858f-7julia> default_tolerance(Float16)
Float16(0.001953)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-7julia> default_ϵ(Float32)
0.0027621358f0SimpleSolvers.direction! — Method
direction!(d, x, s, params, iteration; stalled=false)Compute the Newton direction (for the NewtonSolver). stalled is forwarded to maybe_refactorize!; see needs_refresh.
SimpleSolvers.direction — Method
direction(cache)Return the direction (i.e. the step vector $\Delta{}x$) stored in a solver cache such as NonlinearSolverCache or DogLegCache.
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.22882877718The 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.
SimpleSolvers.direction₁ — Method
SimpleSolvers.direction₂ — Method
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.
SimpleSolvers.dominant_linesearch_outcome — Function
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.
SimpleSolvers.f_stall_factor — Method
f_stall_factor(o::Options)The factor by which the residual has to drop for an iteration to count as progress. See F_STALL_FACTOR and record_progress!.
SimpleSolvers.f_stall_window — Method
f_stall_window(o::Options)The number of iterations without progress after which a NonlinearSolver gives up (0 disables the criterion). See F_STALL_WINDOW and no_progress.
SimpleSolvers.factorization — Method
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)).
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.
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.
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.0Here 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.66667Note 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.
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.
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.
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.
SimpleSolvers.havenonfinite — Method
havenonfinite(status)Check whether any of the three residuals of a NonlinearSolverStatus — rxₛ, 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.
SimpleSolvers.increase_iteration_number! — Method
increase_iteration_number!(state)To be used together with NonlinearSolverState.
SimpleSolvers.initial_residual — Method
initial_residual(state)Return the initial residual ‖F(x₀)‖ recorded by initialize! (NaN if the state has not been initialized). This is the reference scale for the relative-residual convergence test in assess_convergence.
SimpleSolvers.initialize! — Method
SimpleSolvers.initialize! — Method
initialize!(cache, x)Initialize the NonlinearSolverCache with NaNs.
SimpleSolvers.isbenign — Method
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.
SimpleSolvers.isconverged — Method
SimpleSolvers.isfloor — Method
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!).
SimpleSolvers.isnotprogressing — Method
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.
SimpleSolvers.isstalled — Method
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.
SimpleSolvers.issufficient — Method
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.
SimpleSolvers.iterate_settled — Method
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.
SimpleSolvers.iteration_number — Method
iteration_number(state)Return the number of iterations taken so far, as counted by increase_iteration_number! on state::NonlinearSolverState. Compare this to stall_number.
SimpleSolvers.iterations_since_progress — Method
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.
SimpleSolvers.jacobian — Method
jacobian(nlp)Return the Jacobian function stored in the NonlinearProblem nlp.
SimpleSolvers.jacobianmatrix — Method
jacobianmatrix(solver::NonlinearSolver)Return the evaluated Jacobian (a matrix) stored in the cache (see NonlinearSolverCache) of solver.
Also see jacobian(::NonlinearProblem).
SimpleSolvers.linearsolver — Method
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))SimpleSolvers.linesearch_failures — Method
linesearch_failures(state)The number of steps in this solve whose line search reported a failure, i.e. the tally of linesearch_outcomes summed over the outcomes that are not isbenign.
SimpleSolvers.linesearch_failures — Method
linesearch_failures(status)The number of steps whose line search reported a failure, i.e. linesearch_outcomes summed over the outcomes that are not isbenign.
SimpleSolvers.linesearch_index — Method
linesearch_index(oc)The index of the LinesearchOutcome oc in a tally of length NLINESEARCH_OUTCOMES — the enum's value plus one, since the enum starts at zero and Julia indexes from one. See record_linesearch! and linesearch_outcomes.
SimpleSolvers.linesearch_iterations — Method
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)
60julia> linesearch_iterations(Float32)
31SimpleSolvers.linesearch_max_iterations — Method
linesearch_max_iterations(o::Options)The maximum number of trial steps a line search may take within a single solver step. See linesearch_iterations; not to be confused with max_iterations, which bounds the outer nonlinear iteration.
SimpleSolvers.linesearch_outcomes — Method
linesearch_outcomes(state)Return the tally of LinesearchOutcomes reported during this solve, indexed by linesearch_index. Maintained by record_linesearch! and reset per solve by initialize!; NonlinearSolverStatus carries a copy of it out to the caller.
SimpleSolvers.linesearch_outcomes — Method
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)]
0SimpleSolvers.linesearch_problem — Method
linesearch_problem(nl::NonlinearSolver)Build a line search problem based on a NonlinearSolver.
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.375SimpleSolvers.linesearch_problem — Method
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.
SimpleSolvers.linesearch_reason — Function
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.
SimpleSolvers.linesearch_warnings — Function
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.
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:
- a program — a
NonlinearSolver, an optimizer — callssolve_with_status, gets aLinesearchStatusit can act on, and gets no messages. It is not the user, and a diagnosis it can read is worth more to it than one it would have to scrape out of a log. What it does with the status is its own business:solver_step!tallies it (seerecord_linesearch!) and letsnonlinear_solver_warningsexplain the solve once, at the end. - a user calls
solve, which is this path, and a single call yields a single message.
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.
SimpleSolvers.linesearch_αmax — Method
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 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.
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.
SimpleSolvers.lucache_eltype — Method
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.
SimpleSolvers.lucache_matrix — Method
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.
SimpleSolvers.max_stalls — Method
max_stalls(o::Options)The number of consecutive stalled steps after which a NonlinearSolver gives up. See MAX_STALLS and stalled_step.
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.
SimpleSolvers.meets_stopping_criteria — Method
meets_stopping_criteria(state, config)Determines whether the iteration stops based on the current NonlinearSolverState.
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::NonlinearSolverStatusis converged (checked withisconverged) andstate.iterations ≥ config.min_iterations, - the
statushas stagnated (checked withisstalled, i.e.config.max_stallsconsecutive steps that did not move the iterate while the residual is not small) andstate.iterations ≥ config.min_iterations, - the
statusis making no progress (checked withisnotprogressing, i.e.config.f_stall_windowiterations without the residual dropping byconfig.f_stall_factorwhile it is not small) andstate.iterations ≥ config.min_iterations; this is opt-in and never fires at the defaultf_stall_window = 0, status.f_increasedandconfig.allow_f_increases = false(i.e.fincreased even though we do not allow it),state.iterations ≥ config.max_iterations,status.rfₐ > config.f_abstol_break(by defaultInf). In theory this returnstrueif the residual gets too big.- one of the residuals (
rxₛ,rfₐ,rfₛ) is not finite (checked withhavenonfinite) andstate.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)
trueThis obviously has not converged. To check convergence we can use assess_convergence. ```
SimpleSolvers.method — Method
method(ls)Return the method (of type LinearSolverMethod) of the LinearSolver.
SimpleSolvers.method_αmax — Method
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.
SimpleSolvers.minimum_decrease_threshold — Method
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.0001julia> minimum_decrease_threshold(Float32)
0.0001f0SimpleSolvers.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.
SimpleSolvers.needs_refresh — Method
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.
SimpleSolvers.no_progress — Method
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.
SimpleSolvers.nonlinear_solver_warnings — Method
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.
SimpleSolvers.nonlinearproblem — Method
nonlinearproblem(solver)Return the NonlinearProblem contained in the NonlinearSolver. Compare this to linearsolver.
SimpleSolvers.outcome — Method
outcome(status)The LinesearchOutcome stored in status::LinesearchStatus.
SimpleSolvers.pivot_index — Method
pivot_index(v, k)Return the index (starting from k) of the entry of v with the largest absolute value.
This is used for pivoting in factorize!.
SimpleSolvers.print_jacobian — Method
print_jacobian([io], J)Display the Jacobian J as an aligned text/plain table.
Output is written to io (defaulting to stdout).
Here the Jacobian J is a matrix. It is not a Jacobian object.
SimpleSolvers.print_status — Method
print_status(status, config)Print the solver status if:
config.verbosity$\geq1$ and one of the following three
- the solver is converged,
status.iterations ≥ config.max_iterations,status.iterations ≥ config.warn_iterations
config.verbosity$>1.$
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.
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.
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.
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.
SimpleSolvers.report_linesearch_status — Method
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.
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.
SimpleSolvers.residual_small — Method
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.
SimpleSolvers.residuals — Method
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}$).
SimpleSolvers.resolve_jacobian — Method
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.
SimpleSolvers.resolve_linear_solver_method — Method
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.
SimpleSolvers.rhs — Method
rhs(cache)Return the right-hand side of the equation, stored in cache::NonlinearSolverCache.
SimpleSolvers.shift_χ_to_avoid_stalling — Method
shift_χ_to_avoid_stalling(χ, a, b, c, ε)Check whether b is closer to a or c and shift χ accordingly.
This is taken from [4].
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
maxloggot 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.
SimpleSolvers.singular_index — Method
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.
SimpleSolvers.singular_index — Method
singular_index(lsolver::LinearSolver{T,<:PivotedLUMethod})The zero-pivot index the factorization reported (LAPACK's info), or 0 if it succeeded.
SimpleSolvers.singular_index — Method
singular_index(lsolver::LinearSolver{T,<:SparseDirectMethod})0 if the factorization succeeded, non-zero if it did not.
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.
SimpleSolvers.singular_index — Method
singular_index(lsolver::LinearSolver{T,<:LU})The zero-pivot index recorded by factorize! in cache(lsolver).info.
SimpleSolvers.solve! — Function
solve!(x, s, state, params=NullParameters())Solve the NonlinearProblem contained in the NonlinearSolver with the initial condition x.
You also have to supply a NonlinearSolverState.
x is overwritten with the solution and returned. Use solve_with_status! — which is the same iteration, sharing the same body — if the outcome of the solve is needed as well.
SimpleSolvers.solve! — Method
solve!(x, ls::LinearSolver, A, b)Solve the linear system described by:
\[ Ax = b,\]
and store it in x. Here $A$ and $b$ are provided as an input arguments.
Compare this to solve(::LinearSolver, ::AbstractVector).
SimpleSolvers.solve! — Method
solve!(x, ls::LinearSolver, b)Solve the linear system described by:
\[ Ax = b,\]
and store it in x. Here $b$ is provided as an input argument and the factorized $A$ is stored in the LinearSolver ls (respectively its LinearSolverCache).
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
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:
method | constructor keywords |
|---|---|
Newton, QuasiNewton | linesearch, jacobian, linear_solver_method |
DogLeg | jacobian, linear_solver_method |
Picard | jacobian |
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.
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.4142135623730951SimpleSolvers.solve! — Method
solve!(ls::LinearSolver, args...)Solve the LinearProblem with the LinearSolver ls.
SimpleSolvers.solve — Method
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.0SimpleSolvers.solve — Method
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.25Note 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).
SimpleSolvers.solve — Method
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).
SimpleSolvers.solve — Method
solve(method::PivotedLUMethod, ls::LinearProblem)
solve(method::PivotedLUMethod, A, b)Allocate a LinearSolver, factorize and solve in one call.
The counterpart of solve(::LU, ::LinearProblem). Convenient for a one-off system; for a solve inside a loop, build the LinearSolver once and call factorize! and ldiv! on it instead.
SimpleSolvers.solve — Method
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.
SimpleSolvers.solve — Method
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.
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.
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))
trueThe 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
trueSimpleSolvers.solve_with_status — Method
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.
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.
SimpleSolvers.solve_with_status — Method
solve_with_status(ls::Linesearch{T,<:Bisection}, α, params)Bisect the derivative of the merit to approximate the line minimiser and return the LinesearchStatus, emitting no messages. solve is this plus the report; see Bisection.
SimpleSolvers.solve_with_status — Method
solve_with_status(ls::Linesearch{T,<:Backtracking}, α, params)Run the backtracking line search from the trial step α and return the LinesearchStatus, emitting no messages. solve is this plus the report; see Backtracking.
SimpleSolvers.solve_with_status — Method
solve_with_status(ls::Linesearch{T,<:BierlaireQuadratic}, α, params)Fit successive quadratics through three bracketing points to approximate the line minimiser and return the LinesearchStatus, emitting no messages. solve is this plus the report; see BierlaireQuadratic.
SimpleSolvers.solve_with_status — Method
solve_with_status(ls::Linesearch{T,<:Quadratic}, α, params)Fit successive quadratics to approximate the line minimiser and return the LinesearchStatus, emitting no messages. solve is this plus the report; see Quadratic.
SimpleSolvers.solve_with_status — Method
solve_with_status(ls::Linesearch{T,<:StrongWolfe}, α, params)Run the strong-Wolfe bracketing line search from the trial step α and return the LinesearchStatus, emitting no messages. solve is this plus the report; see StrongWolfe.
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.37767096061051814SimpleSolvers.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.
SimpleSolvers.spent_without_progress — Method
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$.
SimpleSolvers.stall_number — Method
stall_number(state)Return the number of consecutive stalled steps recorded in state::NonlinearSolverState by record_stall!. Compare this to iteration_number.
SimpleSolvers.stalled_step — Method
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.".
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!.
SimpleSolvers.status — Method
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))
trueSimpleSolvers.steplength — Method
steplength(status)The step length stored in status::LinesearchStatus, i.e. what solve returns.
SimpleSolvers.trials — Method
trials(status)The number of trial steps at which the merit was evaluated, stored in status::LinesearchStatus.
SimpleSolvers.triple_point_finder — Method
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.
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.
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 off(x₀)(armijo_tolerance), sofdoes 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: eithernmaxdoublings never reached a turning point, orfrose 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].
SimpleSolvers.trust_radius! — Method
trust_radius!(cache::DogLegCache, Δ)Store the trust-region radius $\Delta$ in the DogLegCache so it carries over to the next outer solver step.
SimpleSolvers.trust_radius — Method
trust_radius(cache::DogLegCache)Return the current trust-region radius $\Delta$ carried by the DogLegCache.
SimpleSolvers.value! — Method
value!(y, nlp, x, params)Evaluate the NonlinearProblem at x.
SimpleSolvers.with_config — Method
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.
SimpleSolvers.zero_like — Method
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.