diff --git a/Project.toml b/Project.toml index 67f39e158..f80f230d3 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" ConsoleProgressMonitor = "88cd18e8-d9cc-4ea6-8889-5259c0d15c8b" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" LBFGSB = "5be7bae1-8223-5378-bac3-9e7378a2f6e6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" @@ -43,6 +44,7 @@ Lux = "1.12.4" MLUtils = "0.4" ModelingToolkit = "9" Optim = ">= 1.4.1" +Optimisers = ">= 0.2.5" OptimizationBase = "2" OptimizationMOI = "0.5" OptimizationOptimJL = "0.4" @@ -51,7 +53,7 @@ OrdinaryDiffEqTsit5 = "1" Pkg = "1" Printf = "1.10" ProgressLogging = "0.1" -Random = "1.10" +Random = "1.10" Reexport = "1.2" ReverseDiff = "1" SafeTestsets = "0.1" @@ -63,7 +65,6 @@ Symbolics = "6" TerminalLoggers = "0.1" Test = "1.10" Tracker = "0.2" -Optimisers = ">= 0.2.5" Zygote = "0.6, 0.7" julia = "1.10" @@ -101,7 +102,4 @@ Tracker = "9f7883ad-71c0-57eb-9f7f-b5c9e6d3789c" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [targets] -test = ["Aqua", "BenchmarkTools", "Boltz", "ComponentArrays", "DiffEqFlux", "Enzyme", "FiniteDiff", "Flux", "ForwardDiff", - "Ipopt", "IterTools", "Lux", "MLUtils", "ModelingToolkit", "Optim", "OptimizationMOI", "OptimizationOptimJL", "OptimizationOptimisers", - "OrdinaryDiffEqTsit5", "Pkg", "Random", "ReverseDiff", "SafeTestsets", "SciMLSensitivity", "SparseArrays", "SparseDiffTools", - "Symbolics", "Test", "Tracker", "Zygote"] +test = ["Aqua", "BenchmarkTools", "Boltz", "ComponentArrays", "DiffEqFlux", "Enzyme", "FiniteDiff", "Flux", "ForwardDiff", "Ipopt", "IterTools", "Lux", "MLUtils", "ModelingToolkit", "Optim", "OptimizationMOI", "OptimizationOptimJL", "OptimizationOptimisers", "OrdinaryDiffEqTsit5", "Pkg", "Random", "ReverseDiff", "SafeTestsets", "SciMLSensitivity", "SparseArrays", "SparseDiffTools", "Symbolics", "Test", "Tracker", "Zygote"] diff --git a/lib/OptimizationEvolutionary/src/OptimizationEvolutionary.jl b/lib/OptimizationEvolutionary/src/OptimizationEvolutionary.jl index 5fddace1f..36509e240 100644 --- a/lib/OptimizationEvolutionary/src/OptimizationEvolutionary.jl +++ b/lib/OptimizationEvolutionary/src/OptimizationEvolutionary.jl @@ -97,6 +97,11 @@ function SciMLBase.__solve(cache::OptimizationCache{ P, C } + # Check constraint validation if constraints are present + if !isnothing(cache.f.cons) + Optimization._check_constrained_problem(cache) + end + local x, cur, state function _cb(trace) diff --git a/lib/OptimizationMOI/src/nlp.jl b/lib/OptimizationMOI/src/nlp.jl index c8dea646e..43387e77a 100644 --- a/lib/OptimizationMOI/src/nlp.jl +++ b/lib/OptimizationMOI/src/nlp.jl @@ -518,6 +518,14 @@ function _add_moi_variables!(opt_setup, evaluator::MOIOptimizationNLPEvaluator) end function SciMLBase.__solve(cache::MOIOptimizationNLPCache) + # Check constraint validation for MOI NLP optimizer + if !isnothing(cache.evaluator.f.cons) + if isnothing(cache.evaluator.lcons) || isnothing(cache.evaluator.ucons) + throw(ArgumentError("Constrained optimization problem requires both `lcons` and `ucons` to be provided to OptimizationProblem. " * + "Example: OptimizationProblem(optf, u0, p; lcons=[-Inf], ucons=[0.0])")) + end + end + maxiters = Optimization._check_and_convert_maxiters(cache.solver_args.maxiters) maxtime = Optimization._check_and_convert_maxtime(cache.solver_args.maxtime) opt_setup = __map_optimizer_args(cache, diff --git a/lib/OptimizationNLopt/src/OptimizationNLopt.jl b/lib/OptimizationNLopt/src/OptimizationNLopt.jl index 569cc1790..986936436 100644 --- a/lib/OptimizationNLopt/src/OptimizationNLopt.jl +++ b/lib/OptimizationNLopt/src/OptimizationNLopt.jl @@ -152,6 +152,11 @@ function SciMLBase.__solve(cache::OptimizationCache{ P, C } + # Check constraint validation if this solver supports constraints + if SciMLBase.allowsconstraints(cache.opt) + Optimization._check_constrained_problem(cache) + end + local x _loss = function (θ) diff --git a/lib/OptimizationNOMAD/src/OptimizationNOMAD.jl b/lib/OptimizationNOMAD/src/OptimizationNOMAD.jl index 9bfd28b61..40a170428 100644 --- a/lib/OptimizationNOMAD/src/OptimizationNOMAD.jl +++ b/lib/OptimizationNOMAD/src/OptimizationNOMAD.jl @@ -52,6 +52,14 @@ function SciMLBase.__solve(prob::OptimizationProblem, opt::NOMADOpt; reltol::Union{Number, Nothing} = nothing, cons_method = ExtremeBarrierMethod, kwargs...) + # Check constraint validation for NOMAD + if !isnothing(prob.f.cons) + if isnothing(prob.lcons) || isnothing(prob.ucons) + throw(ArgumentError("Constrained optimization problem requires both `lcons` and `ucons` to be provided to OptimizationProblem. " * + "Example: OptimizationProblem(optf, u0, p; lcons=[-Inf], ucons=[0.0])")) + end + end + local x maxiters = Optimization._check_and_convert_maxiters(maxiters) diff --git a/lib/OptimizationOptimJL/src/OptimizationOptimJL.jl b/lib/OptimizationOptimJL/src/OptimizationOptimJL.jl index 0721d55e3..3109d66a0 100644 --- a/lib/OptimizationOptimJL/src/OptimizationOptimJL.jl +++ b/lib/OptimizationOptimJL/src/OptimizationOptimJL.jl @@ -344,6 +344,9 @@ function SciMLBase.__solve(cache::OptimizationCache{ D, P } + # Check constraint validation for constrained optimizers + Optimization._check_constrained_problem(cache) + local x, cur, state function _cb(trace) diff --git a/lib/OptimizationPRIMA/src/OptimizationPRIMA.jl b/lib/OptimizationPRIMA/src/OptimizationPRIMA.jl index 00973adfa..084c595d7 100644 --- a/lib/OptimizationPRIMA/src/OptimizationPRIMA.jl +++ b/lib/OptimizationPRIMA/src/OptimizationPRIMA.jl @@ -129,6 +129,11 @@ function SciMLBase.__solve(cache::Optimization.OptimizationCache{ P, C } + # Check constraint validation if this solver supports constraints + if SciMLBase.allowsconstraints(cache.opt) + Optimization._check_constrained_problem(cache) + end + iter = 0 _loss = function (θ) x = cache.f(θ, cache.p) diff --git a/lib/OptimizationSciPy/src/OptimizationSciPy.jl b/lib/OptimizationSciPy/src/OptimizationSciPy.jl index d34507c17..c03f6ebc8 100644 --- a/lib/OptimizationSciPy/src/OptimizationSciPy.jl +++ b/lib/OptimizationSciPy/src/OptimizationSciPy.jl @@ -308,6 +308,11 @@ end function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C}) where {F,RC,LB,UB,LC,UC,S,O<:ScipyMinimize,D,P,C} + # Check constraint validation if this solver supports constraints + if SciMLBase.allowsconstraints(cache.opt) + Optimization._check_constrained_problem(cache) + end + local cons_cache = nothing if !isnothing(cache.f.cons) && !isnothing(cache.lcons) cons_cache = zeros(eltype(cache.u0), length(cache.lcons)) @@ -1193,6 +1198,9 @@ end function SciMLBase.__solve(cache::OptimizationCache{F,RC,LB,UB,LC,UC,S,O,D,P,C}) where {F,RC,LB,UB,LC,UC,S,O<:ScipyShgo,D,P,C} + # Check constraint validation for ScipyShgo + Optimization._check_constrained_problem(cache) + local cons_cache = nothing if !isnothing(cache.f.cons) && !isnothing(cache.lcons) cons_cache = zeros(eltype(cache.u0), length(cache.lcons)) diff --git a/src/auglag.jl b/src/auglag.jl index c3b4af753..1e24cc63d 100644 --- a/src/auglag.jl +++ b/src/auglag.jl @@ -80,6 +80,7 @@ function SciMLBase.__solve(cache::OptimizationCache{ solver_kwargs = __map_optimizer_args(cache, cache.opt; maxiters, cache.solver_args...) if !isnothing(cache.f.cons) + _check_constrained_problem(cache) eq_inds = [cache.lcons[i] == cache.ucons[i] for i in eachindex(cache.lcons)] ineq_inds = (!).(eq_inds) diff --git a/src/lbfgsb.jl b/src/lbfgsb.jl index 3cc89c609..a0ab6b313 100644 --- a/src/lbfgsb.jl +++ b/src/lbfgsb.jl @@ -98,6 +98,7 @@ function SciMLBase.__solve(cache::OptimizationCache{ solver_kwargs = __map_optimizer_args(cache, cache.opt; maxiters, cache.solver_args...) if !isnothing(cache.f.cons) + _check_constrained_problem(cache) eq_inds = [cache.lcons[i] == cache.ucons[i] for i in eachindex(cache.lcons)] ineq_inds = (!).(eq_inds) @@ -124,7 +125,8 @@ function SciMLBase.__solve(cache::OptimizationCache{ cache.f.cons(cons_tmp, θ) cons_tmp[eq_inds] .= cons_tmp[eq_inds] - cache.lcons[eq_inds] cons_tmp[ineq_inds] .= cons_tmp[ineq_inds] .- cache.ucons[ineq_inds] - opt_state = Optimization.OptimizationState(u = θ, objective = x[1], p = cache.p, iter = iter_count[]) + opt_state = Optimization.OptimizationState( + u = θ, objective = x[1], p = cache.p, iter = iter_count[]) if cache.callback(opt_state, x...) error("Optimization halted by callback.") end @@ -212,7 +214,8 @@ function SciMLBase.__solve(cache::OptimizationCache{ _loss = function (θ) x = cache.f(θ, cache.p) iter_count[] += 1 - opt_state = Optimization.OptimizationState(u = θ, objective = x[1], p = cache.p, iter = iter_count[]) + opt_state = Optimization.OptimizationState( + u = θ, objective = x[1], p = cache.p, iter = iter_count[]) if cache.callback(opt_state, x...) error("Optimization halted by callback.") end diff --git a/src/utils.jl b/src/utils.jl index 36afe4c23..c51691683 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -52,6 +52,19 @@ function _check_and_convert_maxtime(maxtime) end end +function _check_constrained_problem(cache::OptimizationCache) + """ + Check that if constraints are present, both lcons and ucons are provided. + This validation is called by solvers that support constrained optimization. + """ + if !isnothing(cache.f.cons) + if isnothing(cache.lcons) || isnothing(cache.ucons) + throw(ArgumentError("Constrained optimization problem requires both `lcons` and `ucons` to be provided to OptimizationProblem. " * + "Example: OptimizationProblem(optf, u0, p; lcons=[-Inf], ucons=[0.0])")) + end + end +end + # RetCode handling for BBO and others. using SciMLBase: ReturnCode diff --git a/test/native.jl b/test/native.jl index 0c6c0f6e5..e70f6b3b3 100644 --- a/test/native.jl +++ b/test/native.jl @@ -61,3 +61,52 @@ optf1 = OptimizationFunction(loss, AutoSparseForwardDiff()) prob1 = OptimizationProblem(optf1, rand(5), data) sol1 = solve(prob1, OptimizationOptimisers.Adam(), maxiters = 1000, callback = callback) @test sol1.objective < l0 + +# Test constraint bounds validation (issue #959) +@testset "Constraint bounds validation" begin + # Test case from issue #959 - missing lcons and ucons should give helpful error + rosenbrock_constrained(u, p) = (p[1] - u[1])^2 + p[2] * (u[2] - u[1]^2)^2 + + function cons_missing_bounds!(out, x, p) + out[1] = sum(x) + end + + optf_missing = OptimizationFunction( + rosenbrock_constrained, AutoForwardDiff(), cons = cons_missing_bounds!) + prob_missing = OptimizationProblem(optf_missing, [-1, 1.0], [1.0, 100.0]) + + # Test LBFGS + @test_throws ArgumentError solve(prob_missing, Optimization.LBFGS()) + + # Verify the error message is helpful + try + solve(prob_missing, Optimization.LBFGS()) + catch e + @test isa(e, ArgumentError) + @test occursin("lcons", e.msg) + @test occursin("ucons", e.msg) + @test occursin("OptimizationProblem", e.msg) + @test occursin("Example:", e.msg) + end + + # Test AugLag + @test_throws ArgumentError solve(prob_missing, Optimization.AugLag()) + + # Verify the error message is helpful for AugLag too + try + solve(prob_missing, Optimization.AugLag()) + catch e + @test isa(e, ArgumentError) + @test occursin("lcons", e.msg) + @test occursin("ucons", e.msg) + @test occursin("OptimizationProblem", e.msg) + @test occursin("Example:", e.msg) + end + + # Test that it works when lcons and ucons are provided + prob_with_bounds = OptimizationProblem( + optf_missing, [-1, 1.0], [1.0, 100.0], lcons = [-Inf], ucons = [0.0]) + # This should not throw an error (though it may not converge) + sol = solve(prob_with_bounds, Optimization.LBFGS(), maxiters = 10) + @test !isnothing(sol) +end