Problem classes
Three regimes, kept apart in the type system rather than in comments, because they need three different solvers and admit three different sets of model Hessians and sampling rules.
AbstractProblem
├── DeterministicProblem f(x) evaluated exactly
└── SampledProblem f(x) estimated from a sample
├── ExpectationProblem f(x) = E_ξ[F(x,ξ)]; population unbounded
└── FiniteSumProblem f(x) = (1/M) Σ f_i(x); M finite and known
└── ScoredProblem per-observation gradients available
└── LikelihoodProblem f = −(1/M) Σ ln p_i
└── NLSProblem f = (1/2M) Σ r_i²| class | solver | oracle | cap on N_k | full batch? | truth? |
|---|---|---|---|---|---|
| deterministic | DeterministicTRSolver | any AbstractNLPModel, FullBatchNLP | — | always | always |
| expectation | ExpectationTRSolver | ExpectationNLP | budget | never | only if supplied |
| finite sum | FiniteSumTRSolver | FiniteSumNLP | min(budget, M) | reachable | always |
tr_solve dispatches on the oracle, so the regime is decided by what you built rather than by a keyword you might forget.
Why an expectation is not a finite sum with a large M
The population cap. An expectation has no largest sample, so N_max is a budget the user chooses. A finite sum has exactly M terms, so N_k ≤ M is imposed by the problem, and a user N_max there is either redundant (≥ M) or a deliberate sub-population budget (< M) — two different intentions that should not share a keyword. FiniteSumNLP therefore rejects a rule carrying an N_max and takes budget explicitly:
using TrustRegionRadius, ADNLPModels, NLPModels, LinearAlgebra
base = ADNLPModel(x -> 0.5 * sum(abs2, x .- 1), [0.0, 0.0])
prob = PerturbedSum(base, 500; σg = 1.0, seed = 1)
try
FiniteSumNLP(prob, RadiusProportional(N_max = 500))
catch e
showerror(stdout, e)
endArgumentError: RadiusProportional was given N_max = 500, but on a finite-sum problem the cap is imposed by the problem: N_k ≤ M = 500. Drop N_max. If a sub-population budget is intended, pass it to the solver as `budget = 500`, which records it as an experimental choice rather than a property of the rule.eprob = PerturbedExpectation(base; σg = 1.0)
(FiniteSumNLP(prob, RadiusProportional()) isa FiniteSumNLP, # cap is M
FiniteSumNLP(prob, RadiusProportional(); budget = 500) isa FiniteSumNLP, # deliberate
ExpectationNLP(eprob, RadiusProportional(N_max = 500)) isa ExpectationNLP) # N_max IS the budget(true, true, true)The full-batch limit. At N_k = M a finite-sum iteration is exactly deterministic: ρ̂ = ρ, the stopping test is the real one, and the accuracy hypotheses of the stochastic theory are discharged rather than assumed. An expectation has no such limit, so its hypotheses can never be discharged by sampling harder. FullBatch makes the limit reachable, is rejected on an expectation, and gives the sharpest regression test in the package:
det = tr_solve(FullBatchNLP(prob); rule = RDelta())
fs = tr_solve(FiniteSumNLP(prob, FullBatch()); rule = RDelta())
(det.iter, fs.iter, det.solution ≈ fs.solution)(2, 2, true)That is the only place the sampled and exact code paths can be compared directly, so it pins the resampling and re-evaluation logic against a reference that has none.
The truth. A finite sum can always be evaluated exactly, at one pass, so true_gradient is available for the whole class and true_stop = true works. For an expectation it exists only when the construction supplies the mean — PerturbedExpectation does, a general one does not — so has_truth is a trait and true_stop on a problem without truth is an ArgumentError, not a silent fallback to the batch test.
:full_batch_trajectory records which finite-sum iterations were exact. A run whose tail is all true has entered the deterministic regime and its final ‖ĝ‖ is the real one; a run whose tail is all false never did, and its status is a statement about one batch.
Deterministic is not "finite sum with M = 1"
DeterministicTRSolver has no resample!, no variance estimate, no sample counters and no sampling keyword. Handing it a sampled oracle is a constructor error, and so is TRParams(true_stop = true) — there the batch test is the true test, so confirming it is meaningless rather than merely redundant.
FullBatchNLP is the bridge: deterministic, while underlying_problem still reports the finite-sum problem underneath, so BHHHModel over a likelihood remains legal. That is why it is a separate type rather than a FiniteSumNLP with FullBatch(): the two are numerically identical and structurally different, and the second still carries a sampling rule, a batch, a variance estimate and an RNG that mean nothing.
What the classes buy the model Hessians
required_problem declares the narrowest class a model is defined for, and check_model_problem enforces it at solver construction:
| model | requires | because |
|---|---|---|
BHHHModel, BHHH2Model | LikelihoodProblem | the information identity is a statement about a negative log-likelihood |
GaussNewtonModel | NLSProblem | it needs the residual Jacobian |
| everything else | AbstractProblem | — |
try
tr_solve(ADNLPModel(x -> sum(abs2, x), [1.0, 2.0]); model = BHHHModel())
catch e
showerror(stdout, e)
endArgumentError: BHHHModel is defined only for LikelihoodProblem, but the problem is a plain ADNLPModel with no problem class. Wrap a LikelihoodProblem in FullBatchNLP, FiniteSumNLP or ExpectationNLP, or use ExactHessian or a quasi-Newton model.logistic = LogisticRegression(K = 3, M = 200, seed = 1)
tr_solve(FullBatchNLP(logistic); model = BHHHModel()).status:first_orderThis matters because the failure it prevents is silent. Applied to a problem that is not a likelihood, BHHH still produces a positive semidefinite matrix and the run still converges; nothing in ρ, ‖g‖ or the radius trace reveals that the model is approximating nothing in particular.
Least squares is maximum likelihood under i.i.d. Gaussian errors, and the score rₙ∇rₙ is the log-likelihood score up to σ². So BHHH applies to it as well as Gauss–Newton, and the two can be compared on one problem — worth having, since they discard different terms and fail for different reasons: BHHH on specification, Gauss–Newton on fit.
Belonging to LikelihoodProblem asserts that f is a negative log-likelihood. It does not assert correct specification, which is a property of the data that no type can carry — information_identity_error is how to check that half.
What the classes buy the sampling rules
needs_scoresagainsthas_scores: the inner-product family needsVar(∇Fᵢᵀĝ)and cannot estimate it from a batch gradient. Checked at oracle construction, soInnerProductTeston an unscored problem fails immediately rather than at iteration 1.requires_finite_population:trueonly forFullBatch.check_population_cap: theN_maxrule above.
API
TrustRegionRadius.AbstractProblem — TypeAbstractProblemRoot of the problem-class hierarchy. A concrete problem declares its regime by subtyping DeterministicProblem, ExpectationProblem or FiniteSumProblem, and the solver, the model Hessian and the sampling rule are all checked against that declaration at construction.
Ordinary JSO models (ADNLPModel, CUTEstModel, …) are not subtypes: they are deterministic by default and need no wrapper. underlying_problem returns nothing for them, which is what the compatibility checks test.
TrustRegionRadius.DeterministicProblem — TypeDeterministicProblem <: AbstractProblemf(x) is evaluated exactly. No batch, no sampling rule, no variance estimate.
Solved by DeterministicTRSolver. Most users never subtype this — a plain AbstractNLPModel is treated as deterministic. It exists for the full-batch view of a sampled problem, FullBatchNLP, which is deterministic while still carrying the finite-sum problem underneath so that BHHHModel remains legal.
TrustRegionRadius.SampledProblem — TypeSampledProblem <: AbstractProblemf(x) is estimated from a sample drawn afresh at each iteration. The common supertype of ExpectationProblem and FiniteSumProblem; dispatch on it for anything that applies to both, on the two children for anything that does not.
TrustRegionRadius.ExpectationProblem — TypeExpectationProblem <: SampledProblemf(x) = E_ξ[F(x,ξ)] with an unbounded population: realisations are drawn i.i.d. from a distribution, never from a finite list.
Consequences the type carries:
population(p) == typemax(Int);N_kis capped only by the user's budget.- There is no full-batch iteration and no exact evaluation, so the accuracy hypotheses of the stochastic convergence theory can never be discharged.
has_truth(p)isfalseunless the construction happens to supply the exact mean —PerturbedExpectationdoes, a general one does not.
Solved by ExpectationTRSolver.
TrustRegionRadius.FiniteSumProblem — TypeFiniteSumProblem <: SampledProblemf(x) = (1/M) Σ_{i=1}^{M} f_i(x) with M finite and known: sampling means subsetting 1:M.
Consequences the type carries:
population(p) == M, soN_k ≤ Mis imposed by the problem. A sampling rule used here must not carry a user-suppliedN_max; passbudgettoFiniteSumTRSolverif a sub-population cap is genuinely wanted.N_k = Mis an exact iteration.FullBatchreaches it deliberately, and:full_batch_trajectoryrecords when it happened.true_objectiveandtrue_gradientare always available, at one full pass, sohas_truthistruefor the whole class.
Solved by FiniteSumTRSolver.
TrustRegionRadius.ScoredProblem — TypeScoredProblem <: FiniteSumProblemA finite-sum problem that reports the gradient of each term separately:
scores(p, x, batch) -> n × |batch| Matrix, column i = ∇f_i(x)
loss_terms(p, x, batch) -> |batch| Vector, entry i = f_i(x)The column mean is the gradient, so this is strictly more information than a gradient evaluation and usually costs no more to produce. It is what the inner-product sampling rules and the outer-product model Hessians consume.
Scores sit under FiniteSumProblem because every scored construction here is a dataset. An expectation with per-realisation scores is coherent and would need a parallel branch; has_scores is the trait to dispatch on if you add one.
TrustRegionRadius.LikelihoodProblem — TypeLikelihoodProblem <: ScoredProblemf(x) = −(1/M) Σ ln p_i(x), an average negative log-likelihood.
This is the class in which BHHH is justified, and the reason it is a type rather than a docstring: required_problem(::BHHHModel) === LikelihoodProblem, so BHHHModel over anything else is an ArgumentError at solver construction rather than a silently meaningless matrix.
The justification is the information identity V = −H, which holds for a correctly specified model at the true parameters. Belonging to this type asserts the first half only — that f is a negative log-likelihood and the scores are its per-observation gradients. Whether the model is correctly specified is a statement about the data that no type can carry, which is what information_identity_error is for.
TrustRegionRadius.NLSProblem — TypeNLSProblem <: LikelihoodProblemf(x) = (1/2M) Σ r_i(x)², nonlinear least squares, additionally supplying
residuals(p, x, batch) -> Vector
jacobian(p, x, batch) -> |batch| × n MatrixPlaced under LikelihoodProblem because it is one: least squares is maximum likelihood under i.i.d. Gaussian errors of known variance, and the score r_i ∇r_i is the log-likelihood score up to σ². So BHHH applies here as well as Gauss-Newton, and the two can be compared on one problem — which is worth having, since they discard different terms and fail for different reasons.
required_problem(::GaussNewtonModel) === NLSProblem: the Jacobian is what it needs and nothing above this type has one.
TrustRegionRadius.problem_class — Functionproblem_class(p) -> Symbol:deterministic, :expectation or :finite_sum. Used in error messages and in the benchmark tables, where a run's regime must be reported alongside its cost.
TrustRegionRadius.population — Functionpopulation(p) -> IntThe number of terms available. M for a finite sum, typemax(Int) for an expectation — not an error, because "unbounded" is exactly what the sampling layer needs to know: it is the value N_k is capped against, and capping against typemax(Int) is a no-op.
TrustRegionRadius.n_terms — Functionn_terms(p) -> IntAlias of population, kept because it appears in the default argument of information_identity_error and gauss_newton_error.
TrustRegionRadius.has_scores — Functionhas_scores(p) -> BoolWhether per-observation gradients are available. true for the whole of ScoredProblem and its descendants.
Checked once, at oracle construction, against needs_scores on the sampling rule — the inner-product family needs Var(∇F_iᵀĝ) and cannot estimate it from a batch gradient alone.
TrustRegionRadius.has_truth — Functionhas_truth(p) -> BoolWhether f and ∇f can be evaluated exactly, so a run can be scored on the truth rather than on its own estimates.
true for every FiniteSumProblem — one full pass. For an ExpectationProblem it is false unless the construction supplies the exact mean, which PerturbedExpectation does by design.
This gates TRParams(true_stop = true) and the :true_grad_trajectory entry: requesting either on a problem without truth is an ArgumentError, not a silent fallback to the batch estimate.
TrustRegionRadius.full_batch — Functionfull_batch(p::FiniteSumProblem) -> Vector{Int}The whole index set 1:M. A finite-sum iteration on this batch is exactly deterministic, which is the limit an expectation does not have.
TrustRegionRadius.underlying_problem — Functionunderlying_problem(nlp) -> AbstractProblem or nothingThe AbstractProblem an NLP oracle presents, or nothing for a plain JSO model. This is what the model-Hessian compatibility check dispatches on: a raw ADNLPModel returns nothing, so a model requiring LikelihoodProblem is rejected against it.
TrustRegionRadius.required_problem — Functionrequired_problem(model) -> TypeThe narrowest problem class a ModelHessian is defined for. AbstractProblem — "anything, including a plain NLP" — by default.
The three outer-product models narrow it, which is the point of the hierarchy:
| model | requires | because |
|---|---|---|
BHHHModel, BHHH2Model | LikelihoodProblem | the information identity is a statement about a likelihood |
GaussNewtonModel | NLSProblem | it needs the residual Jacobian |
Checked by check_model_problem at solver construction, so the failure is a constructor error naming both types rather than a matrix that means nothing.
TrustRegionRadius.check_model_problem — Functioncheck_model_problem(model, nlp) -> nothingThrow an ArgumentError if model requires a narrower problem class than nlp provides. Called by all three solver constructors.
TrustRegionRadius.check_rule_problem — Functioncheck_rule_problem(rule, prob) -> nothingThrow an ArgumentError if a sampling rule needs per-observation scores and the problem does not supply them. Called by the two sampled oracle constructors.
TrustRegionRadius.check_population_cap — Functioncheck_population_cap(rule, prob) -> nothingThrow an ArgumentError if a rule used on a FiniteSumProblem carries a user-supplied N_max.
For a finite sum the cap is M, imposed by the problem. A user N_max there is either redundant (≥ M) or a deliberate sub-population budget (< M), and those are different intentions that should not share a keyword — the first hides a misunderstanding, the second is a legitimate experiment. Pass budget to FiniteSumTRSolver for the second.
On an ExpectationProblem nothing is checked: there the population is unbounded and the cap is the user's budget.
TrustRegionRadius.user_cap — Functionuser_cap(rule) -> Int or nothingThe N_max the user supplied, or nothing when they supplied none.
Distinct from sample_cap, which resolves to typemax(Int) so it can be used in arithmetic. This one preserves the difference between "unset" and "set to a large number", which is what check_population_cap needs.
TrustRegionRadius.DeterministicTRSolver — TypeDeterministicTRSolver(nlp; rule, model, subsolver, params, x_ref,
true_curvature, hessian_norm)Trust-region solver for a problem evaluated exactly: any AbstractNLPModel, or the full-batch view FullBatchNLP of a finite sum.
Accepts no sampling rule. It has no resample!, no variance estimate and no sample counters, and any TRParams(true_stop = …) other than :none is rejected — the batch test is the true test here, so confirming it against the truth is meaningless rather than merely redundant.
stats = tr_solve(nlp; rule = RGrad(), model = SR1Model(mem = 5),
subsolver = SteihaugCG(), params = TRParams(tol = 1e-8))TrustRegionRadius.ExpectationTRSolver — TypeExpectationTRSolver(nlp; rule, model, subsolver, params, x_ref,
true_curvature, hessian_norm)Trust-region solver for an ExpectationNLP.
The sampling rule lives on the oracle, not here — it decides what the oracle returns, so it belongs with the oracle. So does the budget: budget is a keyword of ExpectationNLP, not of this constructor. It is a safety cap on N_k applied on top of the rule's own N_max; the accuracy theory imposes none, so without a cap a criticality-anchored mechanism will ask for Θ(Δ_k^{-4}) objective terms and get it.
TRParams(true_stop = :full) and :both require has_truth(prob). That is true for PerturbedExpectation, which supplies the exact mean by construction, and false for a general expectation — where the request is not conservative but impossible, and is refused rather than silently downgraded to the batch test.
:test is available on every expectation: it consults only σ_g and N_k, both of which the oracle already maintains, and asserts nothing it cannot support.
TrustRegionRadius.FiniteSumTRSolver — TypeFiniteSumTRSolver(nlp; rule, model, subsolver, params)Trust-region solver for a FiniteSumNLP.
Every TRParams(true_stop = …) mode is available here: has_truth holds for the whole class, so :full and :both can confirm exactly, at the cost of one full pass per confirmation. :both pays that cost only when the batch cannot settle the question on its own, and is the mode to prefer.
FiniteSumNLP(prob, FullBatch()) under this solver is numerically identical to FullBatchNLP(prob) under DeterministicTRSolver. That is worth knowing, and worth testing: it is the only place where the sampled and exact code paths can be compared iterate for iterate, so it pins the resampling and re-evaluation logic against a reference that has none.
TrustRegionRadius.FullBatchNLP — TypeFullBatchNLP(prob; x0)The exact view of a FiniteSumProblem: every term evaluated at every call.
Deterministic, so it goes to DeterministicTRSolver — while underlying_problem still reports the finite-sum problem, so required_problem checks pass and BHHHModel over a likelihood remains legal. That combination is the reason this is a separate type rather than a FiniteSumNLP with FullBatch(): the two are numerically identical and structurally different, and the second still carries a sampling rule, a batch, a variance estimate and an RNG that mean nothing.
The dense Hessian is cached per iterate: batch_hess is a pure function of x here, and on MLPClassifier it costs O(n) gradient evaluations, so recomputing it inside every hprod! made ExactHessian unusable on the problems the BHHH comparison needs it for.
The former name LikelihoodNLP remains as an alias.
TrustRegionRadius.ExpectationNLP — TypeExpectationNLP(prob, rule; x0, seed = 0, N_init = 8)Oracle for an ExpectationProblem: each resample! draws N_k fresh realisations.
There is no population cap of the problem's own, so the budget comes from the rule's N_max and from ExpectationTRSolver's budget. There is no full-batch iteration and, unless has_truth(prob), no exact gradient — so a run here can be scored only against whatever the construction supplies.
TrustRegionRadius.FiniteSumNLP — TypeFiniteSumNLP(prob, rule; x0, seed = 0, replace = true, N_init = 8, budget = nothing)Oracle for a FiniteSumProblem: each resample! subsets 1:M.
N_k ≤ M always. A rule carrying a user-supplied N_max is rejected here — see check_population_cap — because on a finite sum the cap is a property of the problem; budget is where a deliberate sub-population limit belongs.
:full_batch_trajectory records the iterations at which N_k = M, which are exactly the iterations that were deterministic.
TrustRegionRadius.SampledNLP — TypeSampledNLP{T, V} <: AbstractNLPModel{T, V}Common supertype of ExpectationNLP and FiniteSumNLP. Everything that does not depend on the population — evaluation, the Hessian cache, the counters, the variance estimates, reset_sampling! — is defined once here.
Common random numbers
The batch is redrawn once per iteration, in resample!, and then held fixed. Every evaluation within the iteration — f̂(x_k), f̂(x_k + s_k), and the retrospective f̂ if the rule needs it — uses the same realisations.
That is not an optimisation. With independent draws the variance of f̂(x_k) − f̂(x_k + s_k) does not shrink as ‖s_k‖ → 0, so ρ̂ becomes pure noise at small radii and every mechanism stalls for a reason that has nothing to do with the mechanism.
TrustRegionRadius.population_cap — Functionpopulation_cap(m) -> IntThe largest sample the oracle will ever draw: the solver budget for an expectation, min(budget, M) for a finite sum. N_k is clamped to it after the rule has spoken, and hitting it is counted in m.capped — a run spent at the cap is no longer meeting the accuracy requirement the convergence theory assumes.