Skip to content

Commit

Permalink
Merge pull request #116 from jump-dev/rb/features
Browse files Browse the repository at this point in the history
Add farkas certificate for variable bounds
  • Loading branch information
joaquimg authored Dec 14, 2020
2 parents f864f9a + 5ad939e commit 6f95f15
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 9 deletions.
42 changes: 42 additions & 0 deletions src/MOI/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2567,13 +2567,42 @@ function _dual_multiplier(model::Optimizer)
return MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE ? 1.0 : -1.0
end

"""
_farkas_variable_dual(model::Optimizer, col::Cint)
Return a Farkas dual associated with the variable bounds of `col`.
Compute the Farkas dual as:
ā * x = λ' * A * x <= λ' * b = -β + sum(āᵢ * Uᵢ | āᵢ < 0) + sum(āᵢ * Lᵢ | āᵢ > 0)
The Farkas dual of the variable is ā, and it applies to the upper bound if ā < 0,
and it applies to the lower bound if ā > 0.
"""
function _farkas_variable_dual(model::Optimizer, col::Int64)
nvars = length(model.variable_info)
nrows = length(model.affine_constraint_info)
ncoeffs = Array{Cint}(undef,1)
getcols(model.inner, C_NULL, C_NULL, C_NULL, nrows, ncoeffs, col, col)
ncoeffs_ = ncoeffs[1]
mstart = Array{Cint}(undef,2)
mrwind = Array{Cint}(undef,ncoeffs_)
dmatval = Array{Float64}(undef,ncoeffs_)
getcols(model.inner, mstart, mrwind, dmatval, nrows, ncoeffs, col, col)
return sum(v * model.cached_solution.linear_dual[i + 1] for (i, v) in zip(mrwind, dmatval))
end

function MOI.get(
model::Optimizer, attr::MOI.ConstraintDual,
c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}}
)
_throw_if_optimize_in_progress(model, attr)
MOI.check_result_index_bounds(model, attr)
column = _info(model, c).column
if model.cached_solution.has_dual_certificate
dual = -_farkas_variable_dual(model, column)
return min(dual, 0.0)
end
reduced_cost = model.cached_solution.variable_dual[column]
sense = MOI.get(model, MOI.ObjectiveSense())
# The following is a heuristic for determining whether the reduced cost
Expand Down Expand Up @@ -2601,6 +2630,10 @@ function MOI.get(
_throw_if_optimize_in_progress(model, attr)
MOI.check_result_index_bounds(model, attr)
column = _info(model, c).column
if model.cached_solution.has_dual_certificate
dual = -_farkas_variable_dual(model, column)
return max(dual,0.0)
end
reduced_cost = model.cached_solution.variable_dual[column]
sense = MOI.get(model, MOI.ObjectiveSense())
# The following is a heuristic for determining whether the reduced cost
Expand Down Expand Up @@ -2628,6 +2661,9 @@ function MOI.get(
_throw_if_optimize_in_progress(model, attr)
MOI.check_result_index_bounds(model, attr)
column = _info(model, c).column
if model.cached_solution.has_dual_certificate
return -_farkas_variable_dual(model, column)
end
reduced_cost = model.cached_solution.variable_dual[column]
return _dual_multiplier(model) * reduced_cost
end
Expand All @@ -2639,6 +2675,9 @@ function MOI.get(
_throw_if_optimize_in_progress(model, attr)
MOI.check_result_index_bounds(model, attr)
column = _info(model, c).column
if model.cached_solution.has_dual_certificate
return -_farkas_variable_dual(model, column)
end
reduced_cost = model.cached_solution.variable_dual[column]
return _dual_multiplier(model) * reduced_cost
end
Expand All @@ -2650,6 +2689,9 @@ function MOI.get(
_throw_if_optimize_in_progress(model, attr)
MOI.check_result_index_bounds(model, attr)
row = _info(model, c).row
if model.cached_solution.has_dual_certificate
return model.cached_solution.linear_dual[row]
end
return _dual_multiplier(model) * model.cached_solution.linear_dual[row]
end

Expand Down
2 changes: 1 addition & 1 deletion src/api.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1546,7 +1546,7 @@ Returns the nonzeros in the constraint matrix for the columns in a given range
"""
function getcols(prob::XpressProblem, _mstart, _mrwind, _dmatval, maxcoeffs, ncoeffs, first::Integer, last::Integer)
@checked Lib.XPRSgetcols(prob, _mstart, _mrwind, _dmatval, maxcoeffs, ncoeffs, Cint(first), Cint(last))
@checked Lib.XPRSgetcols(prob, _mstart, _mrwind, _dmatval, maxcoeffs, ncoeffs, Cint(first-1), Cint(last-1))
end

# # Disable 64Bit versions do to reliability issues.
Expand Down
191 changes: 183 additions & 8 deletions test/MathOptInterface/MOI_Wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ end
"solve_qcp_edge_cases",
"solve_qp_edge_cases",

# Issue #105: Farkas dual for variable bound not implemented.
# These tests require extra parameters to obtain certificates.
"solve_farkas_equalto_upper",
"solve_farkas_equalto_lower",
"solve_farkas_lessthan",
Expand All @@ -67,6 +67,14 @@ end
"solve_farkas_variable_lessthan_max",
],
)
MOIT.solve_farkas_equalto_upper(BRIDGED_CERTIFICATE_OPTIMIZER,CONFIG)
MOIT.solve_farkas_equalto_lower(BRIDGED_CERTIFICATE_OPTIMIZER,CONFIG)
MOIT.solve_farkas_lessthan(BRIDGED_CERTIFICATE_OPTIMIZER,CONFIG)
MOIT.solve_farkas_greaterthan(BRIDGED_CERTIFICATE_OPTIMIZER,CONFIG)
MOIT.solve_farkas_interval_upper(BRIDGED_CERTIFICATE_OPTIMIZER,CONFIG)
MOIT.solve_farkas_interval_lower(BRIDGED_CERTIFICATE_OPTIMIZER,CONFIG)
MOIT.solve_farkas_variable_lessthan(BRIDGED_CERTIFICATE_OPTIMIZER,CONFIG)
MOIT.solve_farkas_variable_lessthan_max(BRIDGED_CERTIFICATE_OPTIMIZER,CONFIG)
MOIT.solve_qcp_edge_cases(BRIDGED_OPTIMIZER, CONFIG_LOW_TOL)
MOIT.solve_qp_edge_cases(BRIDGED_OPTIMIZER, CONFIG_LOW_TOL)
# MOIT.delete_soc_variables(OPTIMIZER, CONFIG_LOW_TOL)
Expand All @@ -81,18 +89,13 @@ end
infeas_certificates = false
), [
# These tests require extra parameters to obtain certificates.
"linear8a", "linear8b", "linear8c",
# TODO: This requires an infeasiblity certificate for a variable bound.
"linear12",
"linear8a", "linear8b", "linear8c","linear12",
]
)
MOIT.linear8atest(BRIDGED_CERTIFICATE_OPTIMIZER, CONFIG)
MOIT.linear8btest(BRIDGED_CERTIFICATE_OPTIMIZER, CONFIG)
MOIT.linear8ctest(BRIDGED_CERTIFICATE_OPTIMIZER, CONFIG)

MOIT.linear12test(
BRIDGED_OPTIMIZER, MOIT.TestConfig(infeas_certificates = false)
)
MOIT.linear12test(BRIDGED_CERTIFICATE_OPTIMIZER, CONFIG)
end

@testset "Quadratic tests" begin
Expand Down Expand Up @@ -280,3 +283,175 @@ end
@test MOI.get(model, Xpress.ConstraintConflictStatus(), c2) == false
end
end

@testset "test_farkas_dual_min" begin
model = Xpress.Optimizer(OUTPUTLOG = 0, PRESOLVE = 0)
x = MOI.add_variables(model, 2)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.set(
model,
MOI.ObjectiveFunction{MOI.SingleVariable}(),
MOI.SingleVariable(x[1]),
)
clb = MOI.add_constraint.(
model, MOI.SingleVariable.(x), MOI.GreaterThan(0.0)
)
c = MOI.add_constraint(
model,
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0),
MOI.LessThan(-1.0),
)
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
@test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE
clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
c_dual = MOI.get(model, MOI.ConstraintDual(), c)
@test clb_dual[1] > 1e-6
@test clb_dual[2] > 1e-6
@test c_dual[1] < -1e-6
@test clb_dual[1] -2 * c_dual atol = 1e-6
@test clb_dual[2] -c_dual atol = 1e-6
end

@testset "test_farkas_dual_min_interval" begin
model = Xpress.Optimizer(OUTPUTLOG = 0, PRESOLVE = 0)
x = MOI.add_variables(model, 2)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.set(
model,
MOI.ObjectiveFunction{MOI.SingleVariable}(),
MOI.SingleVariable(x[1]),
)
clb = MOI.add_constraint.(
model, MOI.SingleVariable.(x), MOI.Interval(0.0, 10.0)
)
c = MOI.add_constraint(
model,
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0),
MOI.LessThan(-1.0),
)
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
@test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE
clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
c_dual = MOI.get(model, MOI.ConstraintDual(), c)
@test clb_dual[1] > 1e-6
@test clb_dual[2] > 1e-6
@test c_dual[1] < -1e-6
@test clb_dual[1] -2 * c_dual atol = 1e-6
@test clb_dual[2] -c_dual atol = 1e-6
end

@testset "test_farkas_dual_min_equalto" begin
model = Xpress.Optimizer(OUTPUTLOG = 0, PRESOLVE = 0)
x = MOI.add_variables(model, 2)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.set(
model,
MOI.ObjectiveFunction{MOI.SingleVariable}(),
MOI.SingleVariable(x[1]),
)
clb = MOI.add_constraint.(model, MOI.SingleVariable.(x), MOI.EqualTo(0.0))
c = MOI.add_constraint(
model,
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0),
MOI.LessThan(-1.0),
)
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
@test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE
clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
c_dual = MOI.get(model, MOI.ConstraintDual(), c)
@test clb_dual[1] > 1e-6
@test clb_dual[2] > 1e-6
@test c_dual[1] < -1e-6
@test clb_dual[1] -2 * c_dual atol = 1e-6
@test clb_dual[2] -c_dual atol = 1e-6
end

@testset "test_farkas_dual_min_ii" begin
model = Xpress.Optimizer(OUTPUTLOG = 0, PRESOLVE = 0)
x = MOI.add_variables(model, 2)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.set(
model,
MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-1.0, x[1])], 0.0),
)
clb = MOI.add_constraint.(
model, MOI.SingleVariable.(x), MOI.LessThan(0.0)
)
c = MOI.add_constraint(
model,
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-2.0, -1.0], x), 0.0),
MOI.LessThan(-1.0),
)
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
@test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE
clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
c_dual = MOI.get(model, MOI.ConstraintDual(), c)
@test clb_dual[1] < -1e-6
@test clb_dual[2] < -1e-6
@test c_dual[1] < -1e-6
@test clb_dual[1] 2 * c_dual atol = 1e-6
@test clb_dual[2] c_dual atol = 1e-6
end

@testset "test_farkas_dual_max" begin
model = Xpress.Optimizer(OUTPUTLOG = 0, PRESOLVE = 0)
x = MOI.add_variables(model, 2)
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
MOI.set(
model,
MOI.ObjectiveFunction{MOI.SingleVariable}(),
MOI.SingleVariable(x[1]),
)
clb = MOI.add_constraint.(
model, MOI.SingleVariable.(x), MOI.GreaterThan(0.0)
)
c = MOI.add_constraint(
model,
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], x), 0.0),
MOI.LessThan(-1.0),
)
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
@test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE
clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
c_dual = MOI.get(model, MOI.ConstraintDual(), c)
@test clb_dual[1] > 1e-6
@test clb_dual[2] > 1e-6
@test c_dual[1] < -1e-6
@test clb_dual[1] -2 * c_dual atol = 1e-6
@test clb_dual[2] -c_dual atol = 1e-6
end

@testset "test_farkas_dual_max_ii" begin
model = Xpress.Optimizer(OUTPUTLOG = 0, PRESOLVE = 0)
x = MOI.add_variables(model, 2)
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
MOI.set(
model,
MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(-1.0, x[1])], 0.0),
)
clb = MOI.add_constraint.(
model, MOI.SingleVariable.(x), MOI.LessThan(0.0)
)
c = MOI.add_constraint(
model,
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([-2.0, -1.0], x), 0.0),
MOI.LessThan(-1.0),
)
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE
@test MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE
clb_dual = MOI.get.(model, MOI.ConstraintDual(), clb)
c_dual = MOI.get(model, MOI.ConstraintDual(), c)
@test clb_dual[1] < -1e-6
@test clb_dual[2] < -1e-6
@test c_dual[1] < -1e-6
@test clb_dual[1] 2 * c_dual atol = 1e-6
@test clb_dual[2] c_dual atol = 1e-6
end

0 comments on commit 6f95f15

Please sign in to comment.