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 vector-valued objective functions #497

Merged
merged 4 commits into from
Feb 13, 2023
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 Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"

[compat]
MathOptInterface = "1.7"
MathOptInterface = "1.12"
julia = "1.6"

[extras]
Expand Down
34 changes: 34 additions & 0 deletions src/MOI_wrapper/MOI_multi_objective.jl
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,37 @@ function MOI.get(model::Gurobi.Optimizer, attr::MultiObjectiveValue)
_check_ret(model, ret)
return val[]
end

function MOI.set(
model::Optimizer,
::MOI.ObjectiveFunction{F},
f::F,
) where {F<:MOI.VectorAffineFunction{Float64}}
for (i, fi) in enumerate(MOI.Utilities.eachscalar(f))
MOI.set(model, MultiObjectiveFunction(i), fi)
end
model.objective_type = _VECTOR_AFFINE
return
end

function MOI.get(
model::Optimizer,
::MOI.ObjectiveFunction{MOI.VectorAffineFunction{Float64}},
)
env = GRBgetenv(model)
F = MOI.ScalarAffineFunction{Float64}
f = F[]
for i in 1:MOI.get(model, NumberOfObjectives())
ret = GRBsetintparam(env, "ObjNumber", i - 1)
_check_ret(env, ret)
push!(f, _get_affine_objective(model; is_multiobjective = true))
end
return MOI.Utilities.operate(vcat, Float64, f...)
end

function MOI.supports(
model::Optimizer,
::MOI.ObjectiveFunction{MOI.VectorAffineFunction{Float64}},
)
return true
end
49 changes: 34 additions & 15 deletions src/MOI_wrapper/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ const CleverDicts = MOI.Utilities.CleverDicts
_INTERVAL,
_EQUAL_TO
)
@enum(_ObjectiveType, _SINGLE_VARIABLE, _SCALAR_AFFINE, _SCALAR_QUADRATIC)
@enum(
_ObjectiveType,
_SINGLE_VARIABLE,
_SCALAR_AFFINE,
_SCALAR_QUADRATIC,
_VECTOR_AFFINE,
)

@enum(
_CallbackState,
_CB_NONE,
Expand Down Expand Up @@ -1150,19 +1157,11 @@ function MOI.set(
return
end

function MOI.get(
model::Optimizer,
::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}},
)
if model.objective_type == _SCALAR_QUADRATIC
error(
"Unable to get objective function. Currently: " *
"$(model.objective_type).",
)
end
function _get_affine_objective(model::Optimizer; is_multiobjective::Bool)
_update_if_necessary(model)
dest = zeros(length(model.variable_info))
ret = GRBgetdblattrarray(model, "Obj", 0, length(dest), dest)
name = is_multiobjective ? "ObjN" : "Obj"
ret = GRBgetdblattrarray(model, name, 0, length(dest), dest)
_check_ret(model, ret)
terms = MOI.ScalarAffineTerm{Float64}[]
for (index, info) in model.variable_info
Expand All @@ -1171,11 +1170,25 @@ function MOI.get(
push!(terms, MOI.ScalarAffineTerm(coefficient, index))
end
constant = Ref{Cdouble}()
ret = GRBgetdblattr(model, "ObjCon", constant)
name = is_multiobjective ? "ObjNCon" : "ObjCon"
ret = GRBgetdblattr(model, name, constant)
_check_ret(model, ret)
return MOI.ScalarAffineFunction(terms, constant[])
end

function MOI.get(
model::Optimizer,
::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}},
)
if model.objective_type == _SCALAR_QUADRATIC
error(
"Unable to get objective function. Currently: " *
"$(model.objective_type).",
)
end
return _get_affine_objective(model; is_multiobjective = false)
end

function MOI.set(
model::Optimizer,
::MOI.ObjectiveFunction{F},
Expand Down Expand Up @@ -3091,6 +3104,10 @@ function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue)
attr.result_index - 1,
)
end
N = MOI.get(model, NumberOfObjectives())
if N > 1
return [MOI.get(model, MultiObjectiveValue(i)) for i in 1:N]
end
valueP = Ref{Cdouble}()
key = attr.result_index == 1 ? "ObjVal" : "PoolObjVal"
ret = GRBgetdblattr(model, key, valueP)
Expand Down Expand Up @@ -3409,9 +3426,11 @@ function MOI.get(model::Optimizer, ::MOI.ObjectiveFunctionType)
return MOI.VariableIndex
elseif model.objective_type == _SCALAR_AFFINE
return MOI.ScalarAffineFunction{Float64}
else
@assert model.objective_type == _SCALAR_QUADRATIC
elseif model.objective_type == _SCALAR_QUADRATIC
return MOI.ScalarQuadraticFunction{Float64}
else
@assert model.objective_type == _VECTOR_AFFINE
return MOI.VectorAffineFunction{Float64}
end
end

Expand Down
33 changes: 32 additions & 1 deletion test/MOI/MOI_multiobjective.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ c4: y >= 0.25
@test MOI.get(model, Gurobi.MultiObjectivePriority(2)) == 0

MOI.optimize!(model)
@test MOI.get(model, MOI.ObjectiveValue()) ≈ 1.5
@test MOI.get(model, MOI.ObjectiveValue()) ≈ [1.5, 2.0]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change (albeit, a minor one for someone using the multi-objective interface to Gurobi)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blegat: this is a good enough time as any to release a Gurobi v1.0.0?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we plan any more breaking changes so I don't see any objection

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I'll merge and then make a few other changes before tagging Gurobi v1.0

@test MOI.get(model, MOI.VariablePrimal(), x) ≈ 0.5
@test MOI.get(model, MOI.VariablePrimal(), y) ≈ 0.5

Expand Down Expand Up @@ -101,6 +101,37 @@ c4: y >= 0.25
@test MOI.get(model, Gurobi.MultiObjectiveValue(2)) ≈ BFS[3].f2
end

function test_example_biobjective_knapsack()
p1 = [77.0, 94, 71, 63, 96, 82, 85, 75, 72, 91, 99, 63, 84, 87, 79, 94, 90]
p2 = [65.0, 90, 90, 77, 95, 84, 70, 94, 66, 92, 74, 97, 60, 60, 65, 97, 93]
w = [80.0, 87, 68, 72, 66, 77, 99, 85, 70, 93, 98, 72, 100, 89, 67, 86, 91]
model = Gurobi.Optimizer()
x = MOI.add_variables(model, length(w))
MOI.add_constraint.(model, x, MOI.ZeroOne())
MOI.add_constraint(model, w' * x, MOI.LessThan(900.0))
obj_f = MOI.Utilities.operate(vcat, Float64, p1' * x, p2' * x)
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
MOI.set(model, MOI.ObjectiveFunction{typeof(obj_f)}(), obj_f)
MOI.optimize!(model)
results = Dict(
[955.0, 906.0] => [2, 3, 5, 6, 9, 10, 11, 14, 15, 16, 17],
[948.0, 939.0] => [1, 2, 3, 5, 6, 8, 10, 11, 15, 16, 17],
[934.0, 971.0] => [2, 3, 5, 6, 8, 10, 11, 12, 15, 16, 17],
[918.0, 983.0] => [2, 3, 4, 5, 6, 8, 10, 11, 12, 16, 17],
)
found_non_dominated_point = false
for i in 1:MOI.get(model, MOI.ResultCount())
X = findall(elt -> elt > 0.9, MOI.get.(model, MOI.VariablePrimal(i), x))
Y = MOI.get(model, MOI.ObjectiveValue(i))
if haskey(results, Y)
@test results[Y] == X
found_non_dominated_point = true
end
end
@test found_non_dominated_point
return
end

end

TestMultiobjective.runtests()