Skip to content

Retain the original OptimizationProblem in OptimizationCache (#1191) - #1209

Draft
ChrisRackauckas-Claude wants to merge 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:retain-original-prob-in-cache
Draft

Retain the original OptimizationProblem in OptimizationCache (#1191)#1209
ChrisRackauckas-Claude wants to merge 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:retain-original-prob-in-cache

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member

Summary

Fixes (the Optimization.jl side of) #1191. An OptimizationSolution only carries the solved OptimizationCache in sol.cache. The cache's instantiated f bakes the parameters p into the objective/constraint closures (see OptimizationDIExt.jl, where cons_oop closes over p in a let f=f, p=p block), so the original problem is effectively unrecoverable from a solution. As the reporter notes, that forces a constrained OptimizationAdjoint in SciMLSensitivity to dig into those closures to recover the raw constraints/parameters — brittle.

This stores the original, untouched OptimizationProblem on the cache as a new prob field, so it is recoverable as sol.cache.prob.

What changed

  • OptimizationCache gains a trailing prob::Pr field holding the as-constructed problem.
    • Dispatch-safe: every solver's __solve dispatches on the leading OptimizationCache{O}, and the only two-parameter method (isinplace) uses the leading parameters. Appending a trailing type parameter/field disturbs nothing (no fieldnames/ConstructionBase/@set/custom getproperty enumerates the layout). Aqua.test_unbound_args stays green because the field is typed with the new parameter (prob::Pr).
  • Both positional inner-constructor call sites pass prob: the main keyword constructor (OptimizationBase) and the duplicated constructor in OptimizationPRIMA.
  • OptimizationPRIMA bumps its OptimizationBase compat lower bound to 5.2 (its constructor now requires the new field) and bumps its own version.
  • OptimizationBase 5.1.3 → 5.2.0.

reinit! still only mutates reinit_cache; cache.prob holds the as-constructed problem (use cache.u0/cache.p for current state), as documented on the field.

Companion

A separate SciMLBase PR adds sol.prob/SciMLBase.get_prob(sol) so the issue's literal sol.prob request returns the original problem (it reads sol.cache.prob). This PR is independently useful — sol.cache.prob works today with it.

Tests (run locally)

Added a testset in solve_internals_test.jl that routes a constrained problem through the real OptimizationCache and asserts sol.cache.prob === prob, that the stored problem exposes the raw, non-p-baked objective/constraints (sol.cache.prob.f.cons === consfun, distinct from sol.cache.f.cons), and that p/lcons/ucons are preserved. Verified locally:

  • solve_internals_test.jl (full file): all testsets pass (incl. new 12-assertion set).
  • LBFGSB end-to-end (init/solve! and solve): sol.cache.prob is the original problem.
  • PRIMA COBYLA constrained end-to-end: exercises the duplicated PRIMA constructor.
  • Aqua.test_unbound_args(OptimizationBase): passes.
  • Runic-formatted.

🚧 Draft — please ignore until reviewed by @ChrisRackauckas.

)

`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>
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

Companion SciMLBase PR (adds sol.prob/get_prob on top of this): SciML/SciMLBase.jl#1367. The two are independent — sol.cache.prob works with this PR alone.

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member Author

CI note: all four package test jobs actually pass their teststest (OptimizationBase, 1.11) and test (OptimizationPRIMA, lts) are green, and the logs for the two red ones show OptimizationBase tests passed / OptimizationPRIMA tests passed followed by the job exiting 255 on error: could not lock config file ~/.gitconfig: File exists during post-job cleanup (a git-config lock race between concurrent jobs on the self-hosted runner). So the red is infra-only; the test phases are green, matching the local run (full OptimizationBase suite 751/751, PRIMA COBYLA constrained). Same gitconfig-lock flake explains the downstream reds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants