diff --git a/src/MathOptLazy.jl b/src/MathOptLazy.jl index 28e7426..ade6f07 100644 --- a/src/MathOptLazy.jl +++ b/src/MathOptLazy.jl @@ -196,6 +196,7 @@ mutable struct Optimizer{OT<:MOI.ModelLike} <: MOI.AbstractOptimizer inner::OT algorithm::AbstractAlgorithm lazy::Dict{Tuple{Type,Type},_LazyData} + silent::Bool function Optimizer(inner_fn; kwargs...) inner = MOI.instantiate(inner_fn; kwargs...) @@ -203,6 +204,7 @@ mutable struct Optimizer{OT<:MOI.ModelLike} <: MOI.AbstractOptimizer inner, Iterative(), Dict{Tuple{Type,Type},_LazyData}(), + false, ) end end @@ -220,6 +222,20 @@ end MOI.Utilities.map_indices(::Function, algorithm::AbstractAlgorithm) = algorithm +### MOI.Silent + +MOI.supports(::Optimizer, ::MOI.Silent) = true + +MOI.get(model::Optimizer, ::MOI.Silent) = model.silent + +function MOI.set(model::Optimizer, ::MOI.Silent, value::Bool) + model.silent = value + if MOI.supports(model.inner, MOI.Silent()) + MOI.set(model.inner, MOI.Silent(), value) + end + return +end + ### Fallbacks function MOI.empty!(model::Optimizer) @@ -544,9 +560,17 @@ end MOI.optimize!(model::Optimizer) = _optimize!(model, model.algorithm) +### MathOptLazy.Iterative + function _optimize!(model::Optimizer, ::Iterative) if (undo = _relax_integrality(model.inner)) !== nothing + if !model.silent + println("[MathOptLazy] relaxing binary and integer variables") + end _iterate(model; start = false) + if !model.silent + println("[MathOptLazy] re-enforcing binary and integer variables") + end undo() end _iterate(model; start = true) @@ -554,7 +578,7 @@ function _optimize!(model::Optimizer, ::Iterative) end function _iterate(model::Optimizer; start::Bool) - needs_solve = true + constraints_added = 1 # A white lie to enter the iteration loop x = MOI.get(model, MOI.ListOfVariableIndices()) # TODO(odow): if the solver supports VariablePrimalStart, we will update the # primal starts during the solve process. This is destructive and in-place. @@ -564,26 +588,32 @@ function _iterate(model::Optimizer; start::Bool) # ::Optimizer, but this is a hassle and no one probably cares. Revisit this # decision if it ever becomes a problem. start &= MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex) - while needs_solve - needs_solve = false + while constraints_added > 0 + constraints_added = 0 + if !model.silent + println("[MathOptLazy] solving current subproblem\n") + end MOI.optimize!(model.inner) if MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE # The problem is unbounded, but it might not be if we add more # constraints. for v in values(model.lazy) - needs_solve |= _add_if_unbounded(model, v) + constraints_added += _add_if_unbounded(model, v) end elseif MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT X = Dict(xi => MOI.get(model, MOI.VariablePrimal(), xi) for xi in x) for v in values(model.lazy) - needs_solve |= _add_if_feasible(model, v, X) + constraints_added += _add_if_feasible(model, v, X) end - if start && needs_solve + if start && constraints_added > 0 for (xi, v) in X MOI.set(model, MOI.VariablePrimalStart(), xi, v) end end end + if !model.silent + println("\n[MathOptLazy] added $(constraints_added) constraints") + end end return end @@ -707,7 +737,7 @@ function _add_if_unbounded(model::Optimizer, data::_LazyData) constraints_added += 1 end end - return constraints_added > 0 + return constraints_added end function _add_if_feasible( @@ -715,7 +745,7 @@ function _add_if_feasible( data::_LazyData, x::Dict{MOI.VariableIndex}, ) - needs_solve = false + constraints_added = 0 for (i, (f, s)) in enumerate(data.data) if data.status[i] != _kLAZY_CONSTRAINT_INACTIVE continue @@ -724,12 +754,14 @@ function _add_if_feasible( if MOI.Utilities.distance_to_set(y, s) > 0 data.index[i] = MOI.add_constraint(model.inner, f, s) data.status[i] = _kLAZY_CONSTRAINT_ACTIVE - needs_solve = true + constraints_added += 1 end end - return needs_solve + return constraints_added end +### MathOptLazy.Callback + function _optimize!(model::Optimizer, ::Callback) function callback(cb_data) x = MOI.get(model, MOI.ListOfVariableIndices()) @@ -760,6 +792,8 @@ function _optimize!(model::Optimizer, ::Callback) return end +### MathOptLazy.SolverSpecific + function _optimize!(model::Optimizer{T}, ::SolverSpecific) where {T} return error( """ diff --git a/test/runtests.jl b/test/runtests.jl index 648feca..5d53697 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -383,6 +383,7 @@ end function test_delete_bound() model = MathOptLazy.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) c1 = MOI.add_constraint(model, x, MOI.ZeroOne()) @test MOI.is_valid(model, c1) @@ -423,6 +424,7 @@ end function test_empty() model = MathOptLazy.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) set = MathOptLazy.LazyScalarSet(MOI.EqualTo(1.0)) c = MOI.add_constraint(model, 1.0 * x, set) @@ -438,6 +440,7 @@ end function test_is_valid() model = MathOptLazy.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) set = MathOptLazy.LazyScalarSet(MOI.EqualTo(1.0)) c = MOI.add_constraint(model, 1.0 * x, set) @@ -453,6 +456,7 @@ end function test_list_of_constraint_attributes_set() model = MathOptLazy.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, 2) set = MathOptLazy.LazyScalarSet(MOI.EqualTo(1.0)) c = MOI.add_constraint(model, 1.0 * x[1], set) @@ -468,11 +472,13 @@ end function test_relax_integrality_equal_to() model = MathOptLazy.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) c_z = MOI.add_constraint(model, x, MOI.ZeroOne()) MOI.add_constraint(model, x, MOI.EqualTo(0.5)) undo = MathOptLazy._relax_integrality(model.inner) target = HiGHS.Optimizer() + MOI.set(target, MOI.Silent(), true) y, _ = MOI.add_constrained_variable(target, MOI.EqualTo(0.5)) @test sprint(print, model.inner) == sprint(print, target) undo() @@ -484,11 +490,13 @@ end function test_relax_integrality_interval() model = MathOptLazy.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) c_z = MOI.add_constraint(model, x, MOI.ZeroOne()) MOI.add_constraint(model, x, MOI.Interval(-1.0, 2.0)) undo = MathOptLazy._relax_integrality(model.inner) target = HiGHS.Optimizer() + MOI.set(target, MOI.Silent(), true) y, cy = MOI.add_constrained_variable(target, MOI.Interval(0.0, 1.0)) @test sprint(print, model.inner) == sprint(print, target) undo() @@ -501,10 +509,12 @@ end function test_relax_integrality_no_bound() model = MathOptLazy.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) c_z = MOI.add_constraint(model, x, MOI.ZeroOne()) undo = MathOptLazy._relax_integrality(model.inner) target = HiGHS.Optimizer() + MOI.set(target, MOI.Silent(), true) y = MOI.add_variable(target) c_l = MOI.add_constraint(target, y, MOI.GreaterThan(0.0)) c_u = MOI.add_constraint(target, y, MOI.LessThan(1.0)) @@ -520,11 +530,13 @@ end function test_relax_integrality_greater_than() model = MathOptLazy.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) MOI.add_constraint(model, x, MOI.GreaterThan(-0.5)) c_z = MOI.add_constraint(model, x, MOI.ZeroOne()) undo = MathOptLazy._relax_integrality(model.inner) target = HiGHS.Optimizer() + MOI.set(target, MOI.Silent(), true) y, cy = MOI.add_constrained_variable(target, MOI.GreaterThan(0.0)) c_u = MOI.add_constraint(target, y, MOI.LessThan(1.0)) @test sprint(print, model.inner) == sprint(print, target) @@ -539,11 +551,13 @@ end function test_relax_integrality_less_than() model = MathOptLazy.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) MOI.add_constraint(model, x, MOI.LessThan(1.5)) c_z = MOI.add_constraint(model, x, MOI.ZeroOne()) undo = MathOptLazy._relax_integrality(model.inner) target = HiGHS.Optimizer() + MOI.set(target, MOI.Silent(), true) y, cy = MOI.add_constrained_variable(target, MOI.LessThan(1.0)) c_l = MOI.add_constraint(target, y, MOI.GreaterThan(0.0)) @test sprint(print, model.inner) == sprint(print, target) @@ -558,11 +572,13 @@ end function test_relax_integrality_semi_integer() model = MathOptLazy.Optimizer(HiGHS.Optimizer) + MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) c_z = MOI.add_constraint(model, x, MOI.ZeroOne()) t, _ = MOI.add_constrained_variable(model, MOI.Semiinteger(3.0, 4.0)) undo = MathOptLazy._relax_integrality(model.inner) target = HiGHS.Optimizer() + MOI.set(target, MOI.Silent(), true) y = MOI.add_variable(target) c_l = MOI.add_constraint(target, y, MOI.GreaterThan(0.0)) c_u = MOI.add_constraint(target, y, MOI.LessThan(1.0)) @@ -577,6 +593,49 @@ function test_relax_integrality_semi_integer() return end +function test_print_log() + N = 10 + model = MathOptLazy.Optimizer(HiGHS.Optimizer) + @test MOI.supports(model, MOI.Silent()) + @test MOI.get(model, MOI.Silent()) == false + MOI.set(model, MOI.Silent(), true) + @test MOI.get(model, MOI.Silent()) == true + MOI.set(model, MOI.Silent(), false) + @test MOI.get(model, MOI.Silent()) == false + x = MOI.add_variables(model, N) + for i in 1:N + MOI.add_constraint(model, x[i], MOI.GreaterThan(0.0)) + MOI.add_constraint(model, x[i], MOI.Integer()) + set = MathOptLazy.LazyScalarSet(MOI.LessThan(1.0)) + MOI.add_constraint(model, 1.0 * x[i], set) + end + f = sum(abs(cos(i)) * x[i] for i in 1:N) + MOI.add_constraint(model, f, MOI.LessThan(1.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + g = sum(abs(sin(i)) * x[i] for i in 1:N) + MOI.set(model, MOI.ObjectiveFunction{typeof(g)}(), g) + dir = mktempdir() + open(joinpath(dir, "out.log"), "w") do io + return redirect_stdout(() -> MOI.optimize!(model), io) + end + contents = read(joinpath(dir, "out.log"), String) + for line in [ + "[MathOptLazy] relaxing binary and integer variables", + "[MathOptLazy] solving current subproblem", + "[MathOptLazy] added 1 constraints", + "[MathOptLazy] added 0 constraints", + "[MathOptLazy] re-enforcing binary and integer variables", + ] + @test occursin(line, contents) + end + MOI.set(model, MOI.Silent(), true) + open(joinpath(dir, "out.log"), "w") do io + return redirect_stdout(() -> MOI.optimize!(model), io) + end + @test isempty(read(joinpath(dir, "out.log"), String)) + return +end + end # TestMathOptLazy TestMathOptLazy.runtests()