diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index bdc427f..d953fb3 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -2142,10 +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 * ifelse(di <= 0, ui, li) * 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 @@ -2171,24 +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 * ifelse(di <= 0, ui, li) * 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) - # 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) + else + # Pick the bound depending on the sign of the dual value + 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()