diff --git a/lib/OptimizationBase/Project.toml b/lib/OptimizationBase/Project.toml index a4e082456..489059db0 100644 --- a/lib/OptimizationBase/Project.toml +++ b/lib/OptimizationBase/Project.toml @@ -1,6 +1,6 @@ name = "OptimizationBase" uuid = "bca83a33-5cc9-4baa-983d-23429ab6bcbb" -version = "5.1.3" +version = "5.2.0" authors = ["Vaibhav Dixit and contributors"] [deps] diff --git a/lib/OptimizationBase/src/cache.jl b/lib/OptimizationBase/src/cache.jl index 4206001b1..9d6a45d81 100644 --- a/lib/OptimizationBase/src/cache.jl +++ b/lib/OptimizationBase/src/cache.jl @@ -7,7 +7,7 @@ end struct OptimizationCache{ O, IIP, F <: SciMLBase.AbstractOptimizationFunction{IIP}, - RC, LB, UB, LC, UC, S, P, C, M, V, + RC, LB, UB, LC, UC, S, P, C, M, V, PR, } <: SciMLBase.AbstractOptimizationCache opt::O @@ -24,6 +24,13 @@ struct OptimizationCache{ analysis_results::AnalysisResults solver_args::NamedTuple verbose::V + # Original `OptimizationProblem` the cache was built from. Unlike `f`, which + # holds the instantiated objective/constraints with `p` baked into closures, + # `prob` keeps the user's untouched problem so the original objective, + # constraints, parameters, and bounds remain recoverable from the solution. + # Not updated by `reinit!` (which only mutates `reinit_cache`); use + # `cache.u0`/`cache.p` for the current state. + prob::PR end function OptimizationCache( @@ -111,7 +118,7 @@ function OptimizationCache( prob.ucons, prob.sense, progress, callback, manifold, AnalysisResults(obj_res, cons_res), merge((; maxiters, maxtime, abstol, reltol), NamedTuple(kwargs)), - processed_verbose + processed_verbose, prob ) end diff --git a/lib/OptimizationBase/test/solve_internals_test.jl b/lib/OptimizationBase/test/solve_internals_test.jl index af744b58e..e34786b11 100644 --- a/lib/OptimizationBase/test/solve_internals_test.jl +++ b/lib/OptimizationBase/test/solve_internals_test.jl @@ -187,6 +187,24 @@ function OptimizationBase.__solve( ) end +# Mock solver with has_init = true that also allows constraints, so a constrained +# problem routes through the real OptimizationCache (not DefaultOptimizationCache). +struct MockSolverWithInitCons end +SciMLBase.has_init(::MockSolverWithInitCons) = true +OptimizationBase.allowsconstraints(::MockSolverWithInitCons) = true + +function OptimizationBase.__solve( + cache::OptimizationBase.OptimizationCache{MockSolverWithInitCons} + ) + stats = SciMLBase.OptimizationStats(; iterations = 1, time = 0.0, fevals = 1) + u = cache.reinit_cache.u0 + return SciMLBase.build_solution( + cache, cache.opt, u, cache.f.f(u, cache.reinit_cache.p); + retcode = ReturnCode.Success, + stats = stats + ) +end + # Mock solver that allows constraints (for _check_opt_alg tests) struct MockAlgWithCons end OptimizationBase.allowsconstraints(::MockAlgWithCons) = true @@ -329,3 +347,40 @@ end @test SciMLBase.successful_retcode(sol) @test _mock_ncalls[] == 1 end + +# ============================================================ +# OptimizationCache retains the original OptimizationProblem (issue #1191) +# ============================================================ + +@testset "cache retains the original problem" begin + objfun = (x, p) -> sum(abs2, x) + sum(p) + # Raw, un-instantiated constraint: takes p explicitly and is not p-baked. + consfun = (res, x, p) -> (res .= x[1]^2 + x[2]^2) + f = OptimizationFunction(objfun, SciMLBase.NoAD(); cons = consfun) + p0 = [3.0, 5.0] + prob = OptimizationProblem(f, [0.5, 0.5], p0; lcons = [0.0], ucons = [2.0]) + + # `init` does not remake the problem, so the cache holds it by identity. + cache = init(prob, MockSolverWithInitCons()) + @test cache.prob === prob + + sol = solve!(cache) + # The original problem is recoverable from the solution via the cache. + @test sol.cache.prob === prob + @test sol.cache.prob.p === p0 + @test sol.cache.prob.f.f === objfun + # The original constraint is the raw user function, not the p-baked closure + # that the instantiated cache function carries. + @test sol.cache.prob.f.cons === consfun + @test sol.cache.f.cons !== consfun + @test sol.cache.prob.lcons == [0.0] + @test sol.cache.prob.ucons == [2.0] + + # The end-to-end `solve` path remakes the problem, but the stored problem is + # still an OptimizationProblem carrying the original objective and parameters. + sol2 = solve(prob, MockSolverWithInitCons()) + @test sol2.cache.prob isa OptimizationProblem + @test sol2.cache.prob.f.f === objfun + @test sol2.cache.prob.f.cons === consfun + @test sol2.cache.prob.p == p0 +end diff --git a/lib/OptimizationPRIMA/Project.toml b/lib/OptimizationPRIMA/Project.toml index 7ba56a756..3aae1b719 100644 --- a/lib/OptimizationPRIMA/Project.toml +++ b/lib/OptimizationPRIMA/Project.toml @@ -1,7 +1,7 @@ name = "OptimizationPRIMA" uuid = "72f8369c-a2ea-4298-9126-56167ce9cbc2" authors = ["Vaibhav Dixit and contributors"] -version = "0.3.8" +version = "0.3.9" [deps] OptimizationBase = "bca83a33-5cc9-4baa-983d-23429ab6bcbb" PRIMA = "0a7d04aa-8ac2-47b3-b7a7-9dbd6ad661ed" @@ -16,7 +16,7 @@ ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" [compat] julia = "1.10" -OptimizationBase = "5.1" +OptimizationBase = "5.2" PRIMA = "0.2.0" SciMLBase = "2.122.1, 3" Reexport = "1" diff --git a/lib/OptimizationPRIMA/src/OptimizationPRIMA.jl b/lib/OptimizationPRIMA/src/OptimizationPRIMA.jl index e4a47789c..afc36f357 100644 --- a/lib/OptimizationPRIMA/src/OptimizationPRIMA.jl +++ b/lib/OptimizationPRIMA/src/OptimizationPRIMA.jl @@ -65,7 +65,7 @@ function OptimizationBase.OptimizationCache( progress, callback, nothing, OptimizationBase.OptimizationBase.AnalysisResults(nothing, nothing), merge((; maxiters, maxtime, abstol, reltol), NamedTuple(kwargs)), - processed_verbose + processed_verbose, prob ) end