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²
classsolveroraclecap on N_kfull batch?truth?
deterministicDeterministicTRSolverany AbstractNLPModel, FullBatchNLP—alwaysalways
expectationExpectationTRSolverExpectationNLPbudgetneveronly if supplied
finite sumFiniteSumTRSolverFiniteSumNLPmin(budget, M)reachablealways

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)
end
ArgumentError: 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:

modelrequiresbecause
BHHHModel, BHHH2ModelLikelihoodProblemthe information identity is a statement about a negative log-likelihood
GaussNewtonModelNLSProblemit needs the residual Jacobian
everything elseAbstractProblem—
try
    tr_solve(ADNLPModel(x -> sum(abs2, x), [1.0, 2.0]); model = BHHHModel())
catch e
    showerror(stdout, e)
end
ArgumentError: 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_order

This 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.

`NLSProblem <: LikelihoodProblem` is deliberate

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

API

TrustRegionRadius.AbstractProblem — Type
AbstractProblem

Root 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.

source
TrustRegionRadius.DeterministicProblem — Type
DeterministicProblem <: AbstractProblem

f(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.

source
TrustRegionRadius.ExpectationProblem — Type
ExpectationProblem <: SampledProblem

f(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_k is 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) is false unless the construction happens to supply the exact mean — PerturbedExpectation does, a general one does not.

Solved by ExpectationTRSolver.

source
TrustRegionRadius.FiniteSumProblem — Type
FiniteSumProblem <: SampledProblem

f(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, so N_k ≤ M is imposed by the problem. A sampling rule used here must not carry a user-supplied N_max; pass budget to FiniteSumTRSolver if a sub-population cap is genuinely wanted.
  • N_k = M is an exact iteration. FullBatch reaches it deliberately, and :full_batch_trajectory records when it happened.
  • true_objective and true_gradient are always available, at one full pass, so has_truth is true for the whole class.

Solved by FiniteSumTRSolver.

source
TrustRegionRadius.ScoredProblem — Type
ScoredProblem <: FiniteSumProblem

A 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.

source
TrustRegionRadius.LikelihoodProblem — Type
LikelihoodProblem <: ScoredProblem

f(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.

source
TrustRegionRadius.NLSProblem — Type
NLSProblem <: LikelihoodProblem

f(x) = (1/2M) Σ r_i(x)², nonlinear least squares, additionally supplying

residuals(p, x, batch) -> Vector
jacobian(p, x, batch)  -> |batch| × n Matrix

Placed 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.

source
TrustRegionRadius.problem_class — Function
problem_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.

source
TrustRegionRadius.population — Function
population(p) -> Int

The 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.

source
TrustRegionRadius.has_scores — Function
has_scores(p) -> Bool

Whether 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.

source
TrustRegionRadius.has_truth — Function
has_truth(p) -> Bool

Whether 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.

source
TrustRegionRadius.full_batch — Function
full_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.

source
TrustRegionRadius.underlying_problem — Function
underlying_problem(nlp) -> AbstractProblem or nothing

The 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.

source
TrustRegionRadius.required_problem — Function
required_problem(model) -> Type

The 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:

modelrequiresbecause
BHHHModel, BHHH2ModelLikelihoodProblemthe information identity is a statement about a likelihood
GaussNewtonModelNLSProblemit 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.

source
TrustRegionRadius.check_model_problem — Function
check_model_problem(model, nlp) -> nothing

Throw an ArgumentError if model requires a narrower problem class than nlp provides. Called by all three solver constructors.

source
TrustRegionRadius.check_rule_problem — Function
check_rule_problem(rule, prob) -> nothing

Throw an ArgumentError if a sampling rule needs per-observation scores and the problem does not supply them. Called by the two sampled oracle constructors.

source
TrustRegionRadius.check_population_cap — Function
check_population_cap(rule, prob) -> nothing

Throw 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.

source
TrustRegionRadius.user_cap — Function
user_cap(rule) -> Int or nothing

The 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.

source
TrustRegionRadius.DeterministicTRSolver — Type
DeterministicTRSolver(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))
source
TrustRegionRadius.ExpectationTRSolver — Type
ExpectationTRSolver(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.

source
TrustRegionRadius.FiniteSumTRSolver — Type
FiniteSumTRSolver(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.

The full-batch reference

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.

source
TrustRegionRadius.FullBatchNLP — Type
FullBatchNLP(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.

source
TrustRegionRadius.ExpectationNLP — Type
ExpectationNLP(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.

source
TrustRegionRadius.FiniteSumNLP — Type
FiniteSumNLP(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.

source
TrustRegionRadius.SampledNLP — Type
SampledNLP{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.

source
TrustRegionRadius.population_cap — Function
population_cap(m) -> Int

The 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.

source