Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for MOI.ScalarNonlinearFunction #134

Merged
merged 3 commits into from
Oct 8, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- uses: julia-actions/setup-julia@latest
with:
# Build documentation on Julia 1.9
version: '1'
version: '1.9'
- name: Install dependencies
run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
- name: Build and deploy
Expand Down
30 changes: 13 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,33 +71,29 @@ This model can be formulated using JuMP code as:
```julia
using JuMP, EAGO

m = Model(EAGO.Optimizer)
model = Model(EAGO.Optimizer)

# Define bounded variables
xL = [10.0; 0.0; 0.0; 0.0; 0.0; 85.0; 90.0; 3.0; 1.2; 145.0]
xU = [2000.0; 16000.0; 120.0; 5000.0; 2000.0; 93.0; 95.0; 12.0; 4.0; 162.0]
@variable(m, xL[i] <= x[i=1:10] <= xU[i])
@variable(model, xL[i] <= x[i=1:10] <= xU[i])

# Define nonlinear constraints
@NLconstraint(m, e1, -x[1]*(1.12 + 0.13167*x[8] - 0.00667*(x[8])^2) + x[4] == 0.0)
@NLconstraint(m, e3, -0.001*x[4]*x[9]*x[6]/(98.0 - x[6]) + x[3] == 0.0)
@NLconstraint(m, e4, -(1.098*x[8] - 0.038*(x[8])^2) - 0.325*x[6] + x[7] == 57.425)
@NLconstraint(m, e5, -(x[2] + x[5])/x[1] + x[8] == 0.0)
# Define constraints
@constraint(model, e1, -x[1]*(1.12 + 0.13167*x[8] - 0.00667*(x[8])^2) + x[4] == 0.0)
@constraint(model, e2, -x[1] + 1.22*x[4] - x[5] == 0.0)
@constraint(model, e3, -0.001*x[4]*x[9]*x[6]/(98.0 - x[6]) + x[3] == 0.0)
@constraint(model, e4, -(1.098*x[8] - 0.038*(x[8])^2) - 0.325*x[6] + x[7] == 57.425)
@constraint(model, e5, -(x[2] + x[5])/x[1] + x[8] == 0.0)
@constraint(model, e6, x[9] + 0.222*x[10] == 35.82)
@constraint(model, e7, -3.0*x[7] + x[10] == -133.0)

# Define linear constraints
@constraint(m, e2, -x[1] + 1.22*x[4] - x[5] == 0.0)
@constraint(m, e6, x[9] + 0.222*x[10] == 35.82)
@constraint(m, e7, -3.0*x[7] + x[10] == -133.0)

# Define nonlinear objective
@NLobjective(m, Max, 0.063*x[4]*x[7] - 5.04*x[1] - 0.035*x[2] - 10*x[3] - 3.36*x[5])
# Define objective
@objective(model, Max, 0.063*x[4]*x[7] - 5.04*x[1] - 0.035*x[2] - 10*x[3] - 3.36*x[5])

# Solve the optimization problem
JuMP.optimize!(m)
JuMP.optimize!(model)
```

Special handling has been included for linear/quadratic functions defined using the `@constraint` macro in JuMP and these can generally be expected to perform better than specifying quadratic or linear terms with the `@NLconstraint` macro.

## A Cautionary Note on Global Optimization

As a global optimization platform, EAGO's solvers can be used to find solutions of general nonconvex problems with a guaranteed certificate of optimality. However, global solvers suffer from the curse of dimensionality and therefore their performance is outstripped by convex/local solvers. For users interested in large-scale applications, be warned that problems generally larger than a few variables may prove challenging for certain types of global optimization problems.
Expand Down
1 change: 0 additions & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ IntervalArithmetic = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
McCormick = "53c679d3-6890-5091-8386-c291e8c8aaa1"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"

[compat]
JuMP = "1.12"
Expand Down
55 changes: 46 additions & 9 deletions src/eago_optimizer/moi_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function check_inbounds!(m::Optimizer, vi::VI)
end
return nothing
end
check_inbounds!(m::Optimizer, f::Number) = nothing
check_inbounds!(m::Optimizer, f::SAF) = foreach(x -> check_inbounds!(m, x.variable), f.terms)
check_inbounds!(m::Optimizer, f::VECOFVAR) = foreach(x -> check_inbounds!(m, x), f.variables)
function check_inbounds!(m::Optimizer, f::SQF)
Expand All @@ -31,19 +32,23 @@ function check_inbounds!(m::Optimizer, f::SQF)
end
return nothing
end
function check_inbounds!(m::Optimizer, f::MOI.ScalarNonlinearFunction)
foreach(x -> check_inbounds!(m, x), f.args)
return nothing
end

MOI.supports_constraint(::Optimizer, ::Type{VI}, ::Type{S}) where S <: VAR_SETS = true
MOI.supports_constraint(::Optimizer,::Type{T},::Type{S}) where {T<:Union{SAF,SQF},S<:INEQ_SETS} = true
MOI.supports_constraint(::Optimizer,::Type{T},::Type{S}) where {T<:Union{SAF,SQF,MOI.ScalarNonlinearFunction},S<:INEQ_SETS} = true

MOI.is_valid(m::Optimizer, v::VI) = (1 <= v.value <= m._input_problem._variable_count)
MOI.is_valid(m::Optimizer, c::CI{VI,S}) where S <: VAR_SETS = (1 <= c.value <= m._input_problem._variable_count)
MOI.is_valid(m::Optimizer, c::CI{F,S}) where {F<:Union{SAF,SQF}, S<:INEQ_SETS} = (1 <= c.value <= length(_constraints(m,F,S)))
MOI.is_valid(m::Optimizer, c::CI{F,S}) where {F<:Union{SAF,SQF,MOI.ScalarNonlinearFunction}, S<:INEQ_SETS} = (1 <= c.value <= length(_constraints(m,F,S)))

MOI.get(m::Optimizer, ::MOI.NumberOfConstraints{VI,S}) where S<:VAR_SETS = length(_constraints(m,VI,S))
MOI.get(m::Optimizer, ::MOI.NumberOfConstraints{F,S}) where {F<:Union{SAF,SQF},S<:INEQ_SETS} = length(_constraints(m,F,S))
MOI.get(m::Optimizer, ::MOI.NumberOfConstraints{F,S}) where {F<:Union{SAF,SQF,MOI.ScalarNonlinearFunction},S<:INEQ_SETS} = length(_constraints(m,F,S))

MOI.get(m::Optimizer, ::MOI.ListOfConstraintIndices{VI,S}) where S<:VAR_SETS = collect(keys(_constraints(m,VI,S)))
MOI.get(m::Optimizer, ::MOI.ListOfConstraintIndices{F,S}) where {F<:Union{SAF,SQF},S<:INEQ_SETS} = collect(keys(_constraints(m,F,S)))
MOI.get(m::Optimizer, ::MOI.ListOfConstraintIndices{F,S}) where {F<:Union{SAF,SQF,MOI.ScalarNonlinearFunction},S<:INEQ_SETS} = collect(keys(_constraints(m,F,S)))

function MOI.add_variable(m::Optimizer)
vi = VI(m._input_problem._variable_count += 1)
Expand All @@ -57,10 +62,31 @@ function MOI.add_variable(m::Optimizer, name::String)
return vi
end

function MOI.add_constraint(m::Optimizer, f::F, s::S) where {F<:Union{SAF,SQF},S<:INEQ_SETS}
function MOI.add_constraint(m::Optimizer, f::F, s::S) where {F<:Union{SAF,SQF,MOI.ScalarNonlinearFunction},S<:INEQ_SETS}
check_inbounds!(m, f)
ci = CI{F, S}(m._input_problem._constraint_count += 1)
_constraints(m, F, S)[ci] = (f, s)
if f isa MOI.ScalarNonlinearFunction
if isnothing(m._input_problem._nlp_data)
model = MOI.Nonlinear.Model()
backend = MOI.Nonlinear.SparseReverseMode()
vars = MOI.get(m, MOI.ListOfVariableIndices())
evaluator = MOI.Nonlinear.Evaluator(model, backend, vars)
m._input_problem._nlp_data = MOI.NLPBlockData(evaluator)
end
MOI.Nonlinear.add_constraint(m._input_problem._nlp_data.evaluator.model, f, s)
constraint_bounds = m._input_problem._nlp_data.constraint_bounds
has_objective = m._input_problem._nlp_data.has_objective
if s isa MOI.LessThan
push!(constraint_bounds, MOI.NLPBoundsPair(-Inf, s.upper))
elseif s isa MOI.GreaterThan
push!(constraint_bounds, MOI.NLPBoundsPair(s.lower, Inf))
else
push!(constraint_bounds, MOI.NLPBoundsPair(s.value, s.value))
end
m._input_problem._nlp_data = MOI.NLPBlockData(constraint_bounds, m._input_problem._nlp_data.evaluator, has_objective)
else
_constraints(m, F, S)[ci] = (f, s)
end
return ci
end
function MOI.add_constraint(m::Optimizer, f::VI, s::S) where S<:VAR_SETS
Expand Down Expand Up @@ -230,7 +256,7 @@ MOI.get(m::Optimizer, p::MOI.RawStatusString) = string(m._global_optimizer._end_
#####
MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true
MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true
MOI.supports(::Optimizer, ::MOI.ObjectiveFunction{F}) where {F <: Union{VI, SAF, SQF}} = true
MOI.supports(::Optimizer, ::MOI.ObjectiveFunction{F}) where {F <: Union{VI, SAF, SQF, MOI.ScalarNonlinearFunction}} = true

function MOI.set(m::Optimizer, ::MOI.NLPBlock, nlp_data::MOI.NLPBlockData)
if nlp_data.has_objective
Expand All @@ -239,9 +265,20 @@ function MOI.set(m::Optimizer, ::MOI.NLPBlock, nlp_data::MOI.NLPBlockData)
m._input_problem._nlp_data = nlp_data
end

function MOI.set(m::Optimizer, ::MOI.ObjectiveFunction{T}, f::T) where T <: Union{VI,SAF,SQF}
function MOI.set(m::Optimizer, ::MOI.ObjectiveFunction{T}, f::T) where T <: Union{VI,SAF,SQF,MOI.ScalarNonlinearFunction}
check_inbounds!(m, f)
m._input_problem._objective = f
if f isa MOI.ScalarNonlinearFunction
if isnothing(m._input_problem._nlp_data)
model = MOI.Nonlinear.Model()
backend = MOI.Nonlinear.SparseReverseMode()
vars = MOI.get(m, MOI.ListOfVariableIndices())
evaluator = MOI.Nonlinear.Evaluator(model, backend, vars)
m._input_problem._nlp_data = MOI.NLPBlockData(evaluator)
end
MOI.Nonlinear.set_objective(m._input_problem._nlp_data.evaluator.model, f)
else
m._input_problem._objective = f
end
end
MOI.get(m::Optimizer, ::MOI.ObjectiveFunction{T}) where T <: Union{VI,SAF,SQF} = m._input_problem._objective
MOI.get(m::Optimizer, ::MOI.ObjectiveFunctionType) = typeof(m._input_problem._objective)
Expand Down
35 changes: 28 additions & 7 deletions test/moit_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,46 @@ function test_runtests()
"test_conic_NormOneCone_VectorAffineFunction",
"test_conic_NormOneCone_VectorOfVariables",
"test_conic_linear_VectorAffineFunction",
"test_cpsat_Circuit",
"test_cpsat_CountAtLeast",
"test_cpsat_Table",
"test_linear_Semicontinuous_integration",
"test_linear_Semiinteger_integration",
"test_linear_integer_solve_twice",
"test_linear_integration",
"test_quadratic_duplicate_terms",
"test_quadratic_integration",
"test_quadratic_nonhomogeneous",
"test_quadratic_constraint_LessThan",
"test_quadratic_constraint_GreaterThan",
"test_modification_affine_deletion_edge_cases",
"test_solve_SOS2_add_and_delete",
# MOI bridge issues
"test_basic_VectorNonlinearFunction_AllDifferent",
"test_basic_VectorNonlinearFunction_BinPacking",
"test_basic_VectorNonlinearFunction_Circuit",
"test_basic_VectorNonlinearFunction_Complements",
"test_basic_VectorNonlinearFunction_CountAtLeast",
"test_basic_VectorNonlinearFunction_CountBelongs",
"test_basic_VectorNonlinearFunction_CountDistinct",
"test_basic_VectorNonlinearFunction_CountGreaterThan",
"test_basic_VectorNonlinearFunction_GeometricMeanCone",
"test_basic_VectorNonlinearFunction_HyperRectangle",
"test_basic_VectorNonlinearFunction_Nonnegatives",
"test_basic_VectorNonlinearFunction_Nonpositives",
"test_basic_VectorNonlinearFunction_NormInfinityCone",
"test_basic_VectorNonlinearFunction_NormOneCone",
"test_basic_VectorNonlinearFunction_RotatedSecondOrderCone",
"test_basic_VectorNonlinearFunction_SecondOrderCone",
"test_basic_VectorNonlinearFunction_SOS1",
"test_basic_VectorNonlinearFunction_SOS2",
"test_basic_VectorNonlinearFunction_Table",
"test_basic_VectorNonlinearFunction_Zeros",
# TODO: bug related to unbounded_check
"test_nonlinear_expression_quartic",
# TODO: bug related to reform_epigraph_min
"test_nonlinear_expression_overrides_objective",
# Remove after MOI v1.31.3
"test_nonlinear_expression_hs110",
# TODO: wrong error thrown. Likely (trivial) bug in MOI wrapper
"test_model_LowerBoundAlreadySet",
"test_model_UpperBoundAlreadySet",
],
exclude_tests_after = v"1.22.0",
exclude_tests_after = v"1.31.2",
)
return
end
Expand Down
Loading