diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index d9a603f8..da4cc7a1 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -8,17 +8,21 @@ export DualOptimizer, dual_optimizer function dual_optimizer( optimizer_constructor; coefficient_type::Type{T} = Float64, + kwargs..., ) where {T<:Number} - return () -> DualOptimizer{T}(MOI.instantiate(optimizer_constructor)) + return () -> + DualOptimizer{T}(MOI.instantiate(optimizer_constructor), kwargs...) end struct DualOptimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer dual_problem::DualProblem{T,OT} + assume_min_if_feasibility::Bool function DualOptimizer{T,OT}( - dual_problem::DualProblem{T,OT}, + dual_problem::DualProblem{T,OT}; + assume_min_if_feasibility::Bool = false, ) where {T,OT<:MOI.ModelLike} - return new{T,OT}(dual_problem) + return new{T,OT}(dual_problem, assume_min_if_feasibility) end end @@ -50,11 +54,14 @@ CachingOptimizer state: EMPTY_OPTIMIZER Solver name: Dual model with HiGHS attached ``` """ -function DualOptimizer(dual_optimizer::OT) where {OT<:MOI.ModelLike} - return DualOptimizer{Float64}(dual_optimizer) +function DualOptimizer(dual_optimizer::OT; kwargs...) where {OT<:MOI.ModelLike} + return DualOptimizer{Float64}(dual_optimizer, kwargs...) end -function DualOptimizer{T}(dual_optimizer::OT) where {T,OT<:MOI.ModelLike} +function DualOptimizer{T}( + dual_optimizer::OT; + kwargs..., +) where {T,OT<:MOI.ModelLike} dual_problem = DualProblem{T}( MOI.Bridges.full_bridge_optimizer( MOI.Utilities.CachingOptimizer( @@ -67,7 +74,7 @@ function DualOptimizer{T}(dual_optimizer::OT) where {T,OT<:MOI.ModelLike} # discover the type of # MOI.Utilities.CachingOptimizer(DualizableModel{T}(), dual_optimizer) OptimizerType = typeof(dual_problem.dual_model) - return DualOptimizer{T,OptimizerType}(dual_problem) + return DualOptimizer{T,OptimizerType}(dual_problem, kwargs...) end DualOptimizer() = error("DualOptimizer must have a solver attached") @@ -205,7 +212,11 @@ function MOI.supports_add_constrained_variables( end function MOI.copy_to(dest::DualOptimizer, src::MOI.ModelLike) - dualize(src, dest.dual_problem) + dualize( + src, + dest.dual_problem, + assume_min_if_feasibility = dest.assume_min_if_feasibility, + ) idx_map = MOI.Utilities.IndexMap() for vi in MOI.get(src, MOI.ListOfVariableIndices()) setindex!(idx_map, vi, vi) diff --git a/src/dualize.jl b/src/dualize.jl index 9c490c97..33ac4fd4 100644 --- a/src/dualize.jl +++ b/src/dualize.jl @@ -44,6 +44,11 @@ On each of these methods, the user can provide the following keyword arguments: * `ignore_objective`: a boolean indicating if the objective function should be added to the dual model. This is also useful for bi-level modelling, where the second level model is represented as a KKT in the upper level model. + + * `assume_min_if_feasibility`: a boolean indicating if the objective function + is of type `MOI.FEASIBILITY_SENSE` then the objective is treated as + `MOI.MIN_SENSE`. Therefore, the dual will have a `MOI.MAX_SENSE` objective. + This is set to false by default, to warn users about the outcome. """ function dualize end @@ -54,6 +59,7 @@ function dualize( variable_parameters::Vector{MOI.VariableIndex} = MOI.VariableIndex[], ignore_objective::Bool = false, consider_constrained_variables::Bool = true, + assume_min_if_feasibility::Bool = false, ) return dualize( primal_model, @@ -62,6 +68,7 @@ function dualize( variable_parameters, ignore_objective, consider_constrained_variables, + assume_min_if_feasibility, ) end @@ -72,6 +79,7 @@ function dualize( variable_parameters::Vector{MOI.VariableIndex}, ignore_objective::Bool, consider_constrained_variables::Bool, + assume_min_if_feasibility::Bool = false, ) where {T} # Throws an error if objective function cannot be dualized supported_objective(primal_model) @@ -81,7 +89,11 @@ function dualize( supported_constraints(con_types) # Errors if constraint cant be dualized # Set the dual model objective sense - _set_dual_model_sense(dual_problem.dual_model, primal_model) + _set_dual_model_sense( + dual_problem.dual_model, + primal_model, + assume_min_if_feasibility, + ) # Get primal objective in quadratic form # terms already split considering parameters diff --git a/src/objective_coefficients.jl b/src/objective_coefficients.jl index f227be14..62ba96ca 100644 --- a/src/objective_coefficients.jl +++ b/src/objective_coefficients.jl @@ -11,6 +11,7 @@ Set the dual model objective sense. function _set_dual_model_sense( dual_model::MOI.ModelLike, primal_model::MOI.ModelLike, + assume_min_if_feasibility::Bool, )::Nothing # Get model sense primal_sense = MOI.get(primal_model, MOI.ObjectiveSense()) @@ -19,8 +20,19 @@ function _set_dual_model_sense( MOI.MAX_SENSE elseif primal_sense == MOI.MAX_SENSE MOI.MIN_SENSE - else # primal_sense == MOI.FEASIBILITY_SENSE - error(primal_sense, " is not supported") + elseif primal_sense == MOI.FEASIBILITY_SENSE && assume_min_if_feasibility + # We assume fesibility sense is a Min 0 + # so the dual would be Max ... + MOI.MAX_SENSE + else + error( + "Expected objective sense to be either MIN_SENSE or MAX_SENSE, " * + "got FEASIBILITY_SENSE. It is not possible to decide how to " * + "dualize. Set the sense to either MIN_SENSE or MAX_SENSE to " * + "proceed. Alternatively, set the keyword argument " * + "`assume_min_if_feasibility` to true to assume the dual model " * + "is a minimization problem without setting the sense.", + ) end MOI.set(dual_model, MOI.ObjectiveSense(), dual_sense) return diff --git a/test/Problems/Feasibility/feasibility_problems.jl b/test/Problems/Feasibility/feasibility_problems.jl new file mode 100644 index 00000000..cf0c0ad4 --- /dev/null +++ b/test/Problems/Feasibility/feasibility_problems.jl @@ -0,0 +1,21 @@ +function feasibility_1_test() + #= + min 0 + s.t. + x_1 + 2x_2 <= 3 + x_1 >= 3 + =# + model = TestModel{Float64}() + + X = MOI.add_variables(model, 2) + + MOI.add_constraint( + model, + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 2.0], X), 0.0), + MOI.LessThan(3.0), + ) + + MOI.add_constraint(model, X[1], MOI.GreaterThan(3.0)) + + return model +end diff --git a/test/Tests/test_dualize_feasibility.jl b/test/Tests/test_dualize_feasibility.jl new file mode 100644 index 00000000..080ddb85 --- /dev/null +++ b/test/Tests/test_dualize_feasibility.jl @@ -0,0 +1,60 @@ +@testset "linear problems" begin + @testset "feasibility_1_test" begin + #= + primal + min 0 + s.t. + x_1 >= 3 :y_2 + x_1 + 2x_2 <= 3 :y_3 + dual + max 3y_2 + 3y_3 + s.t. + y_2 >= 0 + y_3 <= 0 + y_2 + y_3 == 0 :x_1 + 2y_3 == 0 :x_2 + =# + primal_model = feasibility_1_test() + + # fail due no objective sense + @test_throws ErrorException Dualization.dualize(primal_model) + + dual = + Dualization.dualize(primal_model, assume_min_if_feasibility = true) + dual_model = dual.dual_model + primal_dual_map = dual.primal_dual_map + + @test MOI.get(dual_model, MOI.NumberOfVariables()) == 2 + list_of_cons = MOI.get(dual_model, MOI.ListOfConstraintTypesPresent()) + @test Set(list_of_cons) == Set( + [ + (MOI.VariableIndex, MOI.GreaterThan{Float64}) + (MOI.VariableIndex, MOI.LessThan{Float64}) + (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) + ], + ) + @test MOI.get( + dual_model, + MOI.NumberOfConstraints{ + MOI.VariableIndex, + MOI.GreaterThan{Float64}, + }(), + ) == 1 + @test MOI.get( + dual_model, + MOI.NumberOfConstraints{MOI.VariableIndex,MOI.LessThan{Float64}}(), + ) == 1 + @test MOI.get( + dual_model, + MOI.NumberOfConstraints{ + MOI.ScalarAffineFunction{Float64}, + MOI.EqualTo{Float64}, + }(), + ) == 2 + obj_type = MOI.get(dual_model, MOI.ObjectiveFunctionType()) + @test obj_type == MOI.ScalarAffineFunction{Float64} + obj = MOI.get(dual_model, MOI.ObjectiveFunction{obj_type}()) + @test MOI.constant(obj) == 0.0 + @test MOI.coefficient.(obj.terms) == [3.0; 3.0] + end +end diff --git a/test/Tests/test_objective_coefficients.jl b/test/Tests/test_objective_coefficients.jl index 68636978..174d9e8b 100644 --- a/test/Tests/test_objective_coefficients.jl +++ b/test/Tests/test_objective_coefficients.jl @@ -4,20 +4,15 @@ # in the LICENSE.md file or at https://opensource.org/licenses/MIT. @testset "objective_coefficients.jl" begin - @testset "_set_dual_model_sense" begin - # ERROR: FEASIBILITY_SENSE is not supported - # @test_throws ErrorException Dualization._set_dual_model_sense(lp11_test(), lp11_test()) - # obj = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(0.0, MOI.VariableIndex(1))], 0.0) - # @test_throws ErrorException Dualization._PrimalObjective{Float64}(obj) - + @testset "set_dual_model_sense" begin model = lp1_test() @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE - Dualization._set_dual_model_sense(model, model) + Dualization._set_dual_model_sense(model, model, false) @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE model = lp4_test() @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE - Dualization._set_dual_model_sense(model, model) + Dualization._set_dual_model_sense(model, model, false) @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE end end diff --git a/test/runtests.jl b/test/runtests.jl index 8c923abd..0deb0dc6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -54,6 +54,7 @@ include("Problems/RSOC/rsoc_problems.jl") include("Problems/SDP/sdp_triangle_problems.jl") include("Problems/Exponential/exponential_cone_problems.jl") include("Problems/Power/power_cone_problems.jl") +include("Problems/Feasibility/feasibility_problems.jl") # Run tests to travis ci include("Tests/test_structures.jl") @@ -63,6 +64,7 @@ include("Tests/test_dual_model_variables.jl") include("Tests/test_dual_sets.jl") include("Tests/test_dualize_conic_linear.jl") include("Tests/test_dualize_linear.jl") +include("Tests/test_dualize_feasibility.jl") include("Tests/test_dualize_soc.jl") include("Tests/test_dualize_rsoc.jl") include("Tests/test_dualize_sdp.jl")