Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"

Expand Down Expand Up @@ -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"]
5 changes: 5 additions & 0 deletions lib/OptimizationEvolutionary/src/OptimizationEvolutionary.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions lib/OptimizationMOI/src/nlp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions lib/OptimizationNLopt/src/OptimizationNLopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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 (θ)
Expand Down
8 changes: 8 additions & 0 deletions lib/OptimizationNOMAD/src/OptimizationNOMAD.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions lib/OptimizationOptimJL/src/OptimizationOptimJL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions lib/OptimizationPRIMA/src/OptimizationPRIMA.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions lib/OptimizationSciPy/src/OptimizationSciPy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions src/auglag.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 5 additions & 2 deletions src/lbfgsb.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 49 additions & 0 deletions test/native.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading