Retain the original OptimizationProblem in OptimizationCache (#1191) - #1209
Retain the original OptimizationProblem in OptimizationCache (#1191)#1209ChrisRackauckas-Claude wants to merge 1 commit into
Conversation
) `OptimizationSolution` only carries the `OptimizationCache` (`sol.cache`), whose instantiated `f` bakes `p` into the objective/constraint closures. That made the original problem effectively unrecoverable from a solution, forcing downstream consumers (e.g. a constrained `OptimizationAdjoint` in SciMLSensitivity) to reach into those closures to get back the raw constraints/parameters, which is brittle. Store the original, untouched `OptimizationProblem` on the cache as a new `prob` field so it is recoverable as `sol.cache.prob`. The field is added as the last struct field with a trailing type parameter, which is dispatch-safe: every solver's `__solve` dispatches on the leading `OptimizationCache{O}` and the only two-parameter method (`isinplace`) uses the leading parameters. Both positional inner-constructor call sites are updated to pass `prob`: the main keyword constructor in OptimizationBase and the duplicated constructor in OptimizationPRIMA. OptimizationPRIMA's `OptimizationBase` compat lower bound is raised to 5.2 accordingly, since its constructor now requires the new field. `reinit!` still only mutates `reinit_cache`; `cache.prob` holds the as-constructed problem (use `cache.u0`/`cache.p` for the current state), as documented on the field. Adds tests in solve_internals_test.jl asserting that, for a constrained problem routed through the real `OptimizationCache`, `sol.cache.prob` is the original problem by identity and exposes the raw (non-`p`-baked) objective and constraints. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Companion SciMLBase PR (adds |
|
CI note: all four package test jobs actually pass their tests — |
Summary
Fixes (the Optimization.jl side of) #1191. An
OptimizationSolutiononly carries the solvedOptimizationCacheinsol.cache. The cache's instantiatedfbakes the parameterspinto the objective/constraint closures (seeOptimizationDIExt.jl, wherecons_oopcloses overpin alet f=f, p=pblock), so the original problem is effectively unrecoverable from a solution. As the reporter notes, that forces a constrainedOptimizationAdjointin SciMLSensitivity to dig into those closures to recover the raw constraints/parameters — brittle.This stores the original, untouched
OptimizationProblemon the cache as a newprobfield, so it is recoverable assol.cache.prob.What changed
OptimizationCachegains a trailingprob::Prfield holding the as-constructed problem.__solvedispatches on the leadingOptimizationCache{O}, and the only two-parameter method (isinplace) uses the leading parameters. Appending a trailing type parameter/field disturbs nothing (nofieldnames/ConstructionBase/@set/customgetpropertyenumerates the layout).Aqua.test_unbound_argsstays green because the field is typed with the new parameter (prob::Pr).prob: the main keyword constructor (OptimizationBase) and the duplicated constructor in OptimizationPRIMA.OptimizationPRIMAbumps itsOptimizationBasecompat lower bound to5.2(its constructor now requires the new field) and bumps its own version.OptimizationBase5.1.3 → 5.2.0.reinit!still only mutatesreinit_cache;cache.probholds the as-constructed problem (usecache.u0/cache.pfor current state), as documented on the field.Companion
A separate SciMLBase PR adds
sol.prob/SciMLBase.get_prob(sol)so the issue's literalsol.probrequest returns the original problem (it readssol.cache.prob). This PR is independently useful —sol.cache.probworks today with it.Tests (run locally)
Added a testset in
solve_internals_test.jlthat routes a constrained problem through the realOptimizationCacheand assertssol.cache.prob === prob, that the stored problem exposes the raw, non-p-baked objective/constraints (sol.cache.prob.f.cons === consfun, distinct fromsol.cache.f.cons), and thatp/lcons/uconsare preserved. Verified locally:solve_internals_test.jl(full file): all testsets pass (incl. new 12-assertion set).init/solve!andsolve):sol.cache.probis the original problem.COBYLAconstrained end-to-end: exercises the duplicated PRIMA constructor.Aqua.test_unbound_args(OptimizationBase): passes.🚧 Draft — please ignore until reviewed by @ChrisRackauckas.