Skip to content

Remove error from feasibility sense #162

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

Merged
merged 8 commits into from
May 4, 2025
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
27 changes: 19 additions & 8 deletions src/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 13 additions & 1 deletion src/dualize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

This should also be a parameter of dual_optimizer

Copy link
Member

Choose a reason for hiding this comment

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

Yep

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

Expand All @@ -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,
Expand All @@ -62,6 +68,7 @@ function dualize(
variable_parameters,
ignore_objective,
consider_constrained_variables,
assume_min_if_feasibility,
)
end

Expand All @@ -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)
Expand All @@ -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
Expand Down
16 changes: 14 additions & 2 deletions src/objective_coefficients.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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
Expand Down
21 changes: 21 additions & 0 deletions test/Problems/Feasibility/feasibility_problems.jl
Original file line number Diff line number Diff line change
@@ -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
60 changes: 60 additions & 0 deletions test/Tests/test_dualize_feasibility.jl
Original file line number Diff line number Diff line change
@@ -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
11 changes: 3 additions & 8 deletions test/Tests/test_objective_coefficients.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down
Loading