Subproblem solvers
All solve
\[\min_{\|s\| \le \Delta} \; g^\top s + \tfrac12 s^\top H s\]
and return active::Bool, true when ‖s‖ = Δ. That flag is worth propagating: the fraction of iterations on which the constraint binds is the observable that separates radius mechanisms whose first-order behaviour is identical.
| solver | needs | handles indefinite H? | size |
|---|---|---|---|
SteihaugCG | B * v only | yes, stops on negative curvature | any |
ExactMS | dense eigendecomposition | yes, including the hard case | n ≤ nmax |
KrylovCG | B * v | no — assumes H ≻ 0 | any |
KrylovCR | B * v | yes, stops on negative curvature | any |
SteihaugCG and the Cauchy point
The first CG direction is -g. A step that leaves the trust region on CG iteration 1 is therefore exactly the Cauchy point, and the model Hessian has had no influence on its direction whatsoever.
This is not a corner case. When the radius is small — a low μ_max in RGradCapped, a low ζ in RDFO — it happens at every iteration, and the method is gradient descent in disguise. Nothing in ρ, ‖g‖ or the radius trace reveals it. cg_step_info does:
using TrustRegionRadius, ADNLPModels, NLPModels
nlp = ADNLPModel(x -> (1 - x[1])^2 + 100(x[2] - x[1]^2)^2, [-1.2, 1.0])
x = [-1.2, 1.0]
g = grad(nlp, x)
info = cg_step_info(SteihaugCG(), ExactHessian(), nlp, x, g, 0.01)
(info.cg_iters, info.active, info.cos_cauchy)(1, true, 1.0)A radius small enough to truncate CG on its first iteration returns the Cauchy point: cg_iters == 1, the constraint active, and cos(s, -g) == 1, so the model Hessian played no part in choosing the direction.
cg_iters == 1 && active together with cos_cauchy ≈ 1 certify the degeneration.
ExactMS and the hard case
Uses the Moré–Sorensen characterisation: s* is optimal iff there is λ ≥ 0 with
\[(B + \lambda I)s^* = -g, \qquad B + \lambda I \succeq 0, \qquad \lambda(\Delta - \|s^*\|) = 0 .\]
‖s(λ)‖ decreases strictly in λ, so the root is bracketed and bisected.
The hard case — g orthogonal to the eigenspace of λ_min(B), where no λ attains ‖s(λ)‖ = Δ — is handled explicitly by adding the right multiple of that eigenvector. This matters with ExactHessian or SR1Model, where indefiniteness is exactly the information one wants to exploit; it cannot arise with a positive definite model, which is why solvers written for that case often omit it.
Requires an eigendecomposition, hence n ≤ nmax (default 200). Larger problems raise rather than silently allocating an n × n array.
Adding a solver
struct MySolver <: SubproblemSolver end
function TrustRegionRadius.solve_subproblem!(::MySolver, model::ModelHessian, nlp,
x, g, Δ, s, Hs, ws::SubWorkspace;
curv = nothing)
B = hessian_op(model, nlp, x)
# ... write the step into s, using ws.r, ws.d, ws.cand, ws.Hd as scratch ...
return norm(s) ≥ (1 - 1e-8) * Δ # active?
endTake the curvature from model, not from nlp: that is what lets one solver serve every model Hessian.
Three optional pieces:
wsis a caller-ownedSubWorkspace, so nothing is allocated per iteration. An eight-argument convenience method allocates one for you, which is right at the REPL and wrong in the loop.Hsis an output. Declarereturns_hprod(::MySolver) = trueif you leaveB·sin it — CG forms it incrementally anyway, and the solver then skips its ownmodel_hprod!, saving one Hessian-vector product per iteration.curvcarries(λ_min, v_min)when the solver has already computed it. Declareneeds_eigenvector(::MySolver) = trueto be given the eigenvector too, rather than recomputing an eigendecomposition the solver has just done.
API
TrustRegionRadius.SubproblemSolver — TypeSubproblemSolverAbstract supertype for trust-region subproblem solvers. A concrete subtype implements
solve_subproblem!(sub, model, nlp, x, g, Δ, s, Hs, ws; curv = nothing) -> Boolsolving min_{‖s‖ ≤ Δ} gᵀs + ½ sᵀHs and writing the step into s.
TrustRegionRadius.SubWorkspace — TypeSubWorkspace(x)Scratch vectors for the subproblem solvers, allocated once per TRSolver and parametric in the problem's vector type.
Four buffers: the CG residual r, the direction d, the trial iterate cand, and Hd = B·d. EigenPoint reuses cand and Hd after the inner solver has finished with them, and the trust-region loop reuses cand after solve_subproblem! has returned, to form the two residual diagnostics without allocating.
It also carries iters, the inner-iteration count of the last solve. That count is what distinguishes a step the model shaped from the Cauchy point: truncated CG returning after one iteration has taken the step along -g and the model Hessian has influenced nothing. 0 for solvers with no meaningful iteration count.
TrustRegionRadius.SteihaugCG — TypeSteihaugCG(; χ = 0.1, θ = 0.5, max_iters = 100)Steihaug–Toint truncated conjugate gradient. Stops on negative curvature, on reaching the boundary, or when the residual satisfies the forcing condition ‖r‖ ≤ min(χ, ‖g‖^θ)‖g‖.
Needs only B * v, so it scales to large problems with any operator-backed model. Accumulates B·s alongside s, so the predicted reduction costs no extra product.
The first CG direction is -g. A step that leaves the region on CG iteration 1 is therefore exactly the Cauchy point, and the model Hessian has had no influence on its direction. When the radius is small — a low μ_max in RGradCapped, a low ζ in RDFO — this happens at every iteration and the method is gradient descent in disguise. cg_iters in the return of cg_step_info reveals it.
TrustRegionRadius.ExactMS — TypeExactMS(; nmax = 200, tol = 1e-12, maxit = 100)Exact step by the Moré–Sorensen characterisation: s* is optimal iff there is λ ≥ 0 with
(B + λI)s* = −g, B + λI ⪰ 0, λ(Δ − ‖s*‖) = 0.‖s(λ)‖ decreases strictly in λ, so the root is bracketed and bisected.
Handles indefinite B, including the hard case (g orthogonal to the eigenspace of λ_min(B), where no λ attains ‖s(λ)‖ = Δ), by adding the right multiple of a single eigenvector from that eigenspace. The previous implementation added τ to every index within tolerance of λ_min, so with a degenerate leftmost eigenvalue of multiplicity m it returned a step of norm √(‖s̄‖² + mτ²) ≠ Δ — and degenerate λ_min is common on symmetric test problems, which is precisely where the hard case arises.
Requires a dense eigendecomposition, hence n ≤ nmax; larger problems raise rather than silently allocating an n × n array.
TrustRegionRadius.KrylovCG — TypeKrylovCG(; atol = 1e-6, rtol = 1e-6, itmax = 0)Krylov.cg with the radius keyword (Steihaug-style truncation). Assumes positive definite B; use KrylovCR otherwise.
TrustRegionRadius.KrylovCR — TypeKrylovCR(; atol = 1e-6, rtol = 1e-6, itmax = 0)Krylov.cr with the radius keyword; handles indefinite B, stepping to the boundary on negative curvature exactly as SteihaugCG does.
The type was previously generated from Krylov.cg_lanczos, which has no radius keyword — Base.kwarg_decl on it lists check_curvature and no radius in any released version — so every call raised a MethodError before doing any work. The docstring's claim that it solved the constrained subproblem was therefore never exercised.
Krylov.cr is the routine in the same package that does accept radius and handles indefinite B, so it is what the type always meant. The alternative, keeping cg_lanczos and rescaling its unconstrained iterate down to ‖s‖ = Δ, was rejected: a scaled Newton-ish direction is not the trust-region step, and on indefinite B the iterate being rescaled can already have diverged.
KrylovCGLanczos remains as a deprecated alias.
TrustRegionRadius.solve_subproblem! — Functionsolve_subproblem!(sub, model, nlp, x, g, Δ, s, Hs, ws; curv = nothing) -> BoolSolve the trust-region subproblem, writing the step into s.
sub: the solvermodel:ModelHessiansupplying the curvaturenlp: the problem, forhessian_opx, g: current iterate and gradientΔ: radiuss: output buffer for the stepHs: output buffer; holdsB·son return iffreturns_hprod(sub)ws:SubWorkspacescratchcurv: optional(λ_min, v_min)already computed by the caller
Returns true if ‖s‖ = Δ.
TrustRegionRadius.cg_step_info — Functioncg_step_info(sub, model, nlp, x, g, Δ) -> (; cg_iters, active, cos_cauchy)Run SteihaugCG and report how the step was produced:
cg_iters: CG iterations takenactive: whether the step ended on the boundarycos_cauchy:cos(s, −g), which is1exactly when the step is the Cauchy point
cg_iters == 1 && active and cos_cauchy ≈ 1 together certify that the model Hessian played no part in choosing the direction.
Now a thin wrapper over _steihaug! rather than a second copy of the recurrence. Allocating, and intended for diagnostics rather than the solver loop.
TrustRegionRadius.returns_hprod — Functionreturns_hprod(sub) -> BoolWhether solve_subproblem! leaves B·s in the Hs buffer. When true the solver skips its own model_hprod! for the predicted reduction, saving one Hessian-vector product per iteration.
TrustRegionRadius.needs_eigenvector — Functionneeds_eigenvector(sub) -> BoolWhether the subsolver wants the leftmost eigenvector in curv. The solver then requests lambda_min_estimate(...; vector = true) when it is computing the curvature anyway, so the estimate is paid for once rather than twice.