Skip to content
Permalink
Browse files

Add support for multiple solutions (#2100)

* WIP: add support for multiple objectives

* Add result to all solution queries

* Add result_count to docs

* Remove result from shadow_price and fix docs

* Update coverage

* Don't pass default argument
  • Loading branch information
odow committed Nov 26, 2019
1 parent 51863fa commit 326017cd81b365e144ff8dfd8f1b92cf69ba65b5
Showing with 297 additions and 74 deletions.
  1. +27 −0 docs/src/solutions.md
  2. +14 −8 src/JuMP.jl
  3. +9 −3 src/aff_expr.jl
  4. +71 −39 src/constraints.jl
  5. +10 −3 src/nlp.jl
  6. +27 −11 src/objective.jl
  7. +10 −0 src/optimizer_interface.jl
  8. +13 −1 src/quad_expr.jl
  9. +17 −9 src/variables.jl
  10. +99 −0 test/generate_and_solve.jl
@@ -219,6 +219,32 @@ lp_objective_perturbation_range
lp_rhs_perturbation_range
```

## Multiple solutions

Some solvers support returning multiple solutions. You can check how many
solutions are available to query using [`result_count`](@ref).

Functions for querying the solutions, e.g., [`primal_status`](@ref) and
[`value`](@ref), all take an additional keyword argument `result` which can be
used to specify which result to return.

```julia
using JuMP
model = Model()
@variable(model, x[1:10] >= 0)
# ... other constraints ...
optimize!(model)
if termination_status(model) != MOI.OPTIMAL
error("The model was not solved correctly.")
end
num_results = result_count(model)
@assert has_values(model; result = num_results)
an_optimal_solution = value.(x; result = num_results)
an_optimal_objective = objective_value(model; result = num_results)
```

## Reference

```@docs
@@ -233,4 +259,5 @@ JuMP.dual
JuMP.solve_time
OptimizeNotCalled
MOI.optimize!
JuMP.result_count
```
@@ -377,23 +377,29 @@ function raw_status(model::Model)
end

"""
primal_status(model::Model)
primal_status(model::Model; result::Int = 1)
Return the status of the most recent primal solution of the solver (i.e., the
MathOptInterface model attribute `PrimalStatus`).
MathOptInterface model attribute `PrimalStatus`) associated with the result
index `result`.
See also: [`result_count`](@ref).
"""
function primal_status(model::Model)
return MOI.get(model, MOI.PrimalStatus())::MOI.ResultStatusCode
function primal_status(model::Model; result::Int = 1)
return MOI.get(model, MOI.PrimalStatus(result))::MOI.ResultStatusCode
end

"""
dual_status(model::Model)
dual_status(model::Model; result::Int = 1)
Return the status of the most recent dual solution of the solver (i.e., the
MathOptInterface model attribute `DualStatus`).
MathOptInterface model attribute `DualStatus`) associated with the result
index `result`.
See also: [`result_count`](@ref).
"""
function dual_status(model::Model)
return MOI.get(model, MOI.DualStatus())::MOI.ResultStatusCode
function dual_status(model::Model; result::Int = 1)
return MOI.get(model, MOI.DualStatus(result))::MOI.ResultStatusCode
end

set_optimize_hook(model::Model, f) = (model.optimize_hook = f)
@@ -312,12 +312,18 @@ function _assert_isfinite(a::AffExpr)
end

"""
value(v::GenericAffExpr)
value(v::GenericAffExpr; result::Int = 1)
Return the value of the `GenericAffExpr` `v` associated with result index
`result` of the most-recent solution returned by the solver.
Evaluate an `GenericAffExpr` given the result returned by a solver.
Replaces `getvalue` for most use cases.
See also: [`result_count`](@ref).
"""
value(a::GenericAffExpr) = value(a, value)
function value(a::GenericAffExpr; result::Int = 1)
return value(a, (x) -> value(x; result = result))
end

function check_belongs_to_model(a::GenericAffExpr, model::AbstractModel)
for variable in keys(a.terms)
@@ -545,14 +545,19 @@ function add_to_function_constant(constraint::ConstraintRef{Model}, value)
end

"""
value(con_ref::ConstraintRef)
value(con_ref::ConstraintRef; result::Int = 1)
Get the primal value of this constraint in the result returned by a solver. That
is, if `con_ref` is the reference of a constraint `func`-in-`set`, it returns the
value of `func` evaluated at the value of the variables (given by
Return the primal value of constraint `con_ref` associated with result index
`result` of the most-recent solution returned by the solver.
That is, if `con_ref` is the reference of a constraint `func`-in-`set`, it
returns the value of `func` evaluated at the value of the variables (given by
[`value(::VariableRef)`](@ref)).
Use [`has_values`](@ref) to check if a result exists before asking for values.
See also: [`result_count`](@ref).
## Note
For scalar contraints, the constant is moved to the `set` so it is not taken
@@ -562,60 +567,82 @@ into account in the primal value of the constraint. For instance, the constraint
evaluation of `2x + 3y`.
```
"""
function value(con_ref::ConstraintRef{Model, <:_MOICON})
return reshape_vector(_constraint_primal(con_ref), con_ref.shape)
function value(con_ref::ConstraintRef{Model, <:_MOICON}; result::Int = 1)
return reshape_vector(_constraint_primal(con_ref, result), con_ref.shape)
end

# Returns the value of MOI.ConstraintPrimal in a type-stable way
function _constraint_primal(
con_ref::ConstraintRef{Model, <:_MOICON{
<:MOI.AbstractScalarFunction, <:MOI.AbstractScalarSet}})::Float64
return MOI.get(con_ref.model, MOI.ConstraintPrimal(), con_ref)
con_ref::ConstraintRef{
Model, <:_MOICON{<:MOI.AbstractScalarFunction, <:MOI.AbstractScalarSet}
},
result::Int
)::Float64
return MOI.get(con_ref.model, MOI.ConstraintPrimal(result), con_ref)
end
function _constraint_primal(
con_ref::ConstraintRef{Model, <:_MOICON{
<:MOI.AbstractVectorFunction, <:MOI.AbstractVectorSet}})::Vector{Float64}
return MOI.get(con_ref.model, MOI.ConstraintPrimal(), con_ref)
con_ref::ConstraintRef{
Model, <:_MOICON{<:MOI.AbstractVectorFunction, <:MOI.AbstractVectorSet}
},
result
)::Vector{Float64}
return MOI.get(con_ref.model, MOI.ConstraintPrimal(result), con_ref)
end

"""
has_duals(model::Model)
has_duals(model::Model; result::Int = 1)
Return true if the solver has a dual solution available to query, otherwise
return false.
Return `true` if the solver has a dual solution in result index `result`
available to query, otherwise return `false`.
See also [`dual`](@ref) and [`shadow_price`](@ref).
See also [`dual`](@ref), [`shadow_price`](@ref), and [`result_count`](@ref).
"""
has_duals(model::Model) = dual_status(model) != MOI.NO_SOLUTION
function has_duals(model::Model; result::Int = 1)
return dual_status(model; result = result) != MOI.NO_SOLUTION
end

"""
dual(con_ref::ConstraintRef)
dual(con_ref::ConstraintRef; result::Int = 1)
Return the dual value of constraint `con_ref` associated with result index
`result` of the most-recent solution returned by the solver.
Get the dual value of this constraint in the result returned by a solver.
Use `has_dual` to check if a result exists before asking for values.
See also [`shadow_price`](@ref).
See also: [`result_count`](@ref), [`shadow_price`](@ref).
"""
function dual(con_ref::ConstraintRef{Model, <:_MOICON})
return reshape_vector(_constraint_dual(con_ref), dual_shape(con_ref.shape))
function dual(con_ref::ConstraintRef{Model, <:_MOICON}; result::Int = 1)
return reshape_vector(
_constraint_dual(con_ref, result),
dual_shape(con_ref.shape)
)
end

# Returns the value of MOI.ConstraintPrimal in a type-stable way
function _constraint_dual(
con_ref::ConstraintRef{Model, <:_MOICON{
<:MOI.AbstractScalarFunction, <:MOI.AbstractScalarSet}})::Float64
return MOI.get(con_ref.model, MOI.ConstraintDual(), con_ref)
con_ref::ConstraintRef{
Model, <:_MOICON{<:MOI.AbstractScalarFunction, <:MOI.AbstractScalarSet}
},
result::Int
)::Float64
return MOI.get(con_ref.model, MOI.ConstraintDual(result), con_ref)
end
function _constraint_dual(
con_ref::ConstraintRef{Model, <:_MOICON{
<:MOI.AbstractVectorFunction, <:MOI.AbstractVectorSet}})::Vector{Float64}
return MOI.get(con_ref.model, MOI.ConstraintDual(), con_ref)
con_ref::ConstraintRef{
Model, <:_MOICON{<:MOI.AbstractVectorFunction, <:MOI.AbstractVectorSet}
},
result::Int
)::Vector{Float64}
return MOI.get(con_ref.model, MOI.ConstraintDual(result), con_ref)
end


"""
shadow_price(con_ref::ConstraintRef)
The change in the objective from an infinitesimal relaxation of the constraint.
Return the change in the objective from an infinitesimal relaxation of the
constraint.
This value is computed from [`dual`](@ref) and can be queried only when
`has_duals` is `true` and the objective sense is `MIN_SENSE` or `MAX_SENSE`
(not `FEASIBILITY_SENSE`). For linear constraints, the shadow prices differ at
@@ -667,30 +694,35 @@ function shadow_price_greater_than_(dual_value, sense::MOI.OptimizationSense)
end
end

function shadow_price(con_ref::ConstraintRef{Model, _MOICON{F, S}}
) where {S <: MOI.LessThan, F}
function shadow_price(
con_ref::ConstraintRef{Model, _MOICON{F, S}}
) where {S <: MOI.LessThan, F}
model = con_ref.model
if !has_duals(model)
error("The shadow price is not available because no dual result is " *
"available.")
end
return shadow_price_less_than_(dual(con_ref),
objective_sense(model))
return shadow_price_less_than_(
dual(con_ref), objective_sense(model)
)
end

function shadow_price(con_ref::ConstraintRef{Model, _MOICON{F, S}}
) where {S <: MOI.GreaterThan, F}
function shadow_price(
con_ref::ConstraintRef{Model, _MOICON{F, S}}
) where {S <: MOI.GreaterThan, F}
model = con_ref.model
if !has_duals(model)
error("The shadow price is not available because no dual result is " *
"available.")
end
return shadow_price_greater_than_(dual(con_ref),
objective_sense(model))
return shadow_price_greater_than_(
dual(con_ref), objective_sense(model)
)
end

function shadow_price(con_ref::ConstraintRef{Model, _MOICON{F, S}}
) where {S <: MOI.EqualTo, F}
function shadow_price(
con_ref::ConstraintRef{Model, _MOICON{F, S}}
) where {S <: MOI.EqualTo, F}
model = con_ref.model
if !has_duals(model)
error("The shadow price is not available because no dual result is " *
@@ -1118,11 +1118,18 @@ function value(ex::NonlinearExpression, var_value::Function)
end

"""
value(ex::NonlinearExpression)
value(ex::NonlinearExpression; result::Int = 1)
Evaluate `ex` using `value` as the value for each variable `v`.
Return the value of the `NonlinearExpression` `ex` associated with result index
`result` of the most-recent solution returned by the solver.
Replaces `getvalue` for most use cases.
See also: [`result_count`](@ref).
"""
value(ex::NonlinearExpression) = value(ex, value)
function value(ex::NonlinearExpression; result::Int = 1)
return value(ex, (x) -> value(x; result = result))
end

mutable struct _UserFunctionEvaluator <: MOI.AbstractNLPEvaluator
f
@@ -15,23 +15,36 @@
Return the best known bound on the optimal objective value after a call to
`optimize!(model)`.
"""
objective_bound(model::Model)::Float64 = MOI.get(model, MOI.ObjectiveBound())
function objective_bound(model::Model)::Float64
return MOI.get(model, MOI.ObjectiveBound())
end

"""
objective_value(model::Model)
objective_value(model::Model; result::Int = 1)
Return the objective value associated with result index `result` of the
most-recent solution returned by the solver.
Return the objective value after a call to `optimize!(model)`.
See also: [`result_count`](@ref).
"""
objective_value(model::Model)::Float64 = MOI.get(model, MOI.ObjectiveValue())
function objective_value(model::Model; result::Int = 1)::Float64
return MOI.get(model, MOI.ObjectiveValue(result))
end

"""
dual_objective_value(model::Model)
dual_objective_value(model::Model; result::Int = 1)
Return the value of the objective of the dual problem after a call to
`optimize!(model)`. Throws `MOI.UnsupportedAttribute{MOI.DualObjectiveValue}` if
the solver does not support this attribute.
Return the value of the objective of the dual problem associated with result
index `result` of the most-recent solution returned by the solver.
Throws `MOI.UnsupportedAttribute{MOI.DualObjectiveValue}` if the solver does
not support this attribute.
See also: [`result_count`](@ref).
"""
dual_objective_value(model::Model)::Float64 = MOI.get(model, MOI.DualObjectiveValue())
function dual_objective_value(model::Model; result::Int = 1)::Float64
return MOI.get(model, MOI.DualObjectiveValue(result))
end

"""
objective_sense(model::Model)::MathOptInterface.OptimizationSense
@@ -91,8 +104,11 @@ function set_objective_function(model::Model, func::Real)
MOI.ScalarAffineTerm{Float64}[], Float64(func)))
end

function set_objective(model::Model, sense::MOI.OptimizationSense,
func::Union{AbstractJuMPScalar, Real})
function set_objective(
model::Model,
sense::MOI.OptimizationSense,
func::Union{AbstractJuMPScalar, Real}
)
set_objective_sense(model, sense)
set_objective_function(model, func)
end
@@ -169,3 +169,13 @@ function optimize!(model::Model,

return
end

"""
result_count(model::Model)
Return the number of results available to query after a call to
[`optimize!`](@ref).
"""
function result_count(model::Model)::Int
return MOI.get(model, MOI.ResultCount())
end
@@ -452,4 +452,16 @@ function value(ex::GenericQuadExpr{CoefType, VarType},
return ret
end

JuMP.value(ex::JuMP.GenericQuadExpr) = value(ex, JuMP.value)
"""
value(v::GenericQuadExpr; result::Int = 1)
Return the value of the `GenericQuadExpr` `v` associated with result index
`result` of the most-recent solution returned by the solver.
Replaces `getvalue` for most use cases.
See also: [`result_count`](@ref).
"""
function value(ex::GenericQuadExpr; result::Int = 1)
return value(ex, (x) -> value(x; result = result))
end

0 comments on commit 326017c

Please sign in to comment.
You can’t perform that action at this time.