From a93bf8c6afa6abf06000a819da60fa64a7d61b05 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 22 Oct 2024 10:14:05 +1300 Subject: [PATCH 1/2] Fix DualObjectiveValue The dual ray could have the incorrect sign (by tolerance), which would result in the wrong bound being multiplied if only one side is finite. --- src/MOI_wrapper.jl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index bdc427f..28c7dd2 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -2142,7 +2142,8 @@ function MOI.get(model::Optimizer, attr::MOI.DualObjectiveValue) for (li, i, ui) in zip(lower, set, upper) di = model.solution.coldual[i+1] if model.solution.has_dual_ray - dual_objective_value += sense * ifelse(di <= 0, ui, li) * di + dual_objective_value += + sense * _dual_objective_contribution(li, NaN, ui, di) else xi = model.solution.colvalue[i+1] dual_objective_value += _dual_objective_contribution(li, xi, ui, di) @@ -2171,7 +2172,8 @@ function MOI.get(model::Optimizer, attr::MOI.DualObjectiveValue) for (li, i, ui) in zip(lower, set, upper) di = model.solution.rowdual[i+1] if model.solution.has_dual_ray - dual_objective_value += sense * ifelse(di <= 0, ui, li) * di + dual_objective_value += + sense * _dual_objective_contribution(li, NaN, ui, di) else ri = model.solution.rowvalue[i+1] dual_objective_value += _dual_objective_contribution(li, ri, ui, di) @@ -2182,8 +2184,13 @@ end function _dual_objective_contribution(l, x, u, d) if isfinite(l) && isfinite(u) - # Pick the bound that is closest to the primal value - return ifelse(abs(x - l) < abs(x - u), l, u) * d + if isfinite(x) + # Pick the bound that is closest to the primal value + return ifelse(abs(x - l) < abs(x - u), l, u) * d + else + # Pick the bound depending on the sign of the dual value + return ifelse(d >= 0, l, u) * d + end elseif isfinite(l) return l * d else From 0ee2cc614b35d6883e99da1957c018fea1fbed14 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 22 Oct 2024 10:42:55 +1300 Subject: [PATCH 2/2] Update --- src/MOI_wrapper.jl | 20 +++++++++----------- test/MOI_wrapper.jl | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 28c7dd2..d953fb3 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -2142,11 +2142,10 @@ function MOI.get(model::Optimizer, attr::MOI.DualObjectiveValue) for (li, i, ui) in zip(lower, set, upper) di = model.solution.coldual[i+1] if model.solution.has_dual_ray - dual_objective_value += - sense * _dual_objective_contribution(li, NaN, ui, di) + dual_objective_value += sense * _active_bound(li, NaN, ui, di) * di else xi = model.solution.colvalue[i+1] - dual_objective_value += _dual_objective_contribution(li, xi, ui, di) + dual_objective_value += _active_bound(li, xi, ui, di) * di end end # Row components of the dual objective value @@ -2172,30 +2171,29 @@ function MOI.get(model::Optimizer, attr::MOI.DualObjectiveValue) for (li, i, ui) in zip(lower, set, upper) di = model.solution.rowdual[i+1] if model.solution.has_dual_ray - dual_objective_value += - sense * _dual_objective_contribution(li, NaN, ui, di) + dual_objective_value += sense * _active_bound(li, NaN, ui, di) * di else ri = model.solution.rowvalue[i+1] - dual_objective_value += _dual_objective_contribution(li, ri, ui, di) + dual_objective_value += _active_bound(li, ri, ui, di) * di end end return dual_objective_value end -function _dual_objective_contribution(l, x, u, d) +function _active_bound(l, x, u, d) if isfinite(l) && isfinite(u) if isfinite(x) # Pick the bound that is closest to the primal value - return ifelse(abs(x - l) < abs(x - u), l, u) * d + return ifelse(abs(x - l) < abs(x - u), l, u) else # Pick the bound depending on the sign of the dual value - return ifelse(d >= 0, l, u) * d + return ifelse(d >= 0, l, u) end elseif isfinite(l) - return l * d + return l else @assert isfinite(u) - return u * d + return u end end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 48bdfc9..d9a6ee0 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -974,6 +974,32 @@ function test_callback_interrupt() return end +function test_active_bound() + for ((l, x, u, d), result) in [ + # Primal exists. Pick closest bound lower + (0.0, 0.0, 1.0, 2.0) => 0.0, + (0.0, 0.4, 1.0, 2.0) => 0.0, + (0.0, 0.4, 1.0, -2.0) => 0.0, # incorrect d but doesn't matter + # Primal exists. Pick closest bound upper + (0.0, 1.0, 1.0, -2.0) => 1.0, + (0.0, 0.6, 1.0, -2.0) => 1.0, + (0.0, 0.6, 1.0, 2.0) => 1.0, # incorrect d but doesn't matter + # It's a ray. Choose based on sign + (0.0, NaN, 1.0, 2.0) => 0.0, + (0.0, NaN, 1.0, 1e-10) => 0.0, + (0.0, NaN, 1.0, -2.0) => 1.0, + (0.0, NaN, 1.0, -1e-10) => 1.0, + # It's a one-sided ray + (0.0, NaN, Inf, 2.0) => 0.0, + (0.0, NaN, Inf, -1e-10) => 0.0, + (-Inf, NaN, 1.0, 1e-10) => 1.0, + (-Inf, NaN, 1.0, -2.0) => 1.0, + ] + @test HiGHS._active_bound(l, x, u, d) == result + end + return +end + end # module TestMOIHighs.runtests()