diff --git a/src/Algorithm/Algorithm.jl b/src/Algorithm/Algorithm.jl index db1a1aa8e..141cc1016 100644 --- a/src/Algorithm/Algorithm.jl +++ b/src/Algorithm/Algorithm.jl @@ -31,13 +31,11 @@ include("formstorages.jl") include("interface.jl") # Basic algorithms -include("basic/solveipform.jl") include("basic/solvelpform.jl") +include("basic/solveipform.jl") include("basic/cutcallback.jl") -include("basic/pricingcallback.jl") # Child algorithms used by conquer algorithms -include("pricing.jl") include("colgenstabilization.jl") include("colgen.jl") include("benders.jl") @@ -72,7 +70,7 @@ export getterminationstatus, setterminationstatus!, # Algorithm's types export AbstractOptimizationAlgorithm, TreeSearchAlgorithm, ColCutGenConquer, ColumnGeneration, - DefaultPricing, PricingCallback, BendersConquer, BendersCutGeneration, SolveIpForm, + BendersConquer, BendersCutGeneration, SolveIpForm, MoiOptimize, UserOptimizer, SolveLpForm, ExactBranchingPhase, OnlyRestrictedMasterBranchingPhase, PreprocessAlgorithm, RestrictedMasterIPHeuristic, OptimizationInput, OptimizationOutput, OptimizationState, EmptyInput diff --git a/src/Algorithm/basic/pricingcallback.jl b/src/Algorithm/basic/pricingcallback.jl deleted file mode 100644 index cb46b6bd6..000000000 --- a/src/Algorithm/basic/pricingcallback.jl +++ /dev/null @@ -1,50 +0,0 @@ -""" -todo -""" -@with_kw struct PricingCallback <: AbstractOptimizationAlgorithm - stage::Int = 1 # stage 1 is the exact stage by convention, - # any other stage is heuristic - max_nb_ip_primal_sols = 50 -end - -function get_units_usage(algo::PricingCallback, spform::Formulation{DwSp}) - units_usage = Tuple{AbstractModel, UnitType, UnitAccessMode}[] - push!(units_usage, (spform, StaticVarConstrUnit, READ_ONLY)) - return units_usage -end - -function run!(algo::PricingCallback, env::Env, spform::Formulation{DwSp}, input::OptimizationInput)::OptimizationOutput - result = OptimizationState( - spform, - ip_primal_bound = get_ip_primal_bound(getoptstate(input)), - max_length_ip_primal_sols = algo.max_nb_ip_primal_sols - ) - - @logmsg LogLevel(-2) "Calling user-defined optimization function." - - optimizer = getuseroptimizer(spform) - cbdata = MathProg.PricingCallbackData(spform, algo.stage) - optimizer.user_oracle(cbdata) - - if length(cbdata.primal_solutions) > 0 - for primal_sol in cbdata.primal_solutions - add_ip_primal_sol!(result, primal_sol) - end - - if algo.stage == 1 # stage 1 is exact by convention - dual_bound = getvalue(get_ip_primal_bound(result)) - set_ip_dual_bound!(result, DualBound(spform, dual_bound)) - setterminationstatus!(result, OPTIMAL) - else - setterminationstatus!(result, OTHER_LIMIT) - end - else - if algo.stage == 1 - setterminationstatus!(result, INFEASIBLE) - else - setterminationstatus!(result, OTHER_LIMIT) - end - end - - return OptimizationOutput(result) -end diff --git a/src/Algorithm/basic/solveipform.jl b/src/Algorithm/basic/solveipform.jl index 275997873..f8784bccc 100644 --- a/src/Algorithm/basic/solveipform.jl +++ b/src/Algorithm/basic/solveipform.jl @@ -1,29 +1,114 @@ +################################################################################ +# Parameters for each type of optimizer +################################################################################ +""" + MoiOptimize( + time_limit = 600 + deactivate_artificial_vars = false + enforce_integrality = false + get_dual_bound = true + ) + +User parameters for an optimizer that calls a subsolver through MathOptInterface. +""" +@with_kw struct MoiOptimize + time_limit::Int = 600 + deactivate_artificial_vars::Bool = true + enforce_integrality::Bool = true + get_dual_bound::Bool = true + max_nb_ip_primal_sols::Int = 50 + log_level::Int = 2 + silent::Bool = true +end + +""" + UserOptimize( + stage = 1 + max_nb_ip_primal_sols = 50 + ) + +User parameters for an optimizer that calls a callback to solve the problem. +""" +@with_kw struct UserOptimize + stage::Int = 1 + max_nb_ip_primal_sols::Int = 50 +end + +""" + CustomOptimize() + +User parameters for an optimizer that calls a custom solver to solve a custom model. +""" +struct CustomOptimize end + +################################################################################ +# Algorithm +################################################################################ """ Coluna.Algorithm.SolveIpForm( - time_limit::Int = 600, - deactivate_artificial_vars = true, - enforce_integrality = true, - silent = true, - max_nb_ip_primal_sols = 50, - log_level = 0 + optimizer_id = 1 + moi_params = MoiOptimize() + user_params = UserOptimize() + custom_params = CustomOptimize() ) -Solve a mixed integer linear program. +Solve an optimization problem. It can call a : +- subsolver through MathOptInterface to optimize a mixed integer program +- pricing callback defined by the user +- custom optimizer to solve a custom model + +The algorithms calls optimizer with id `optimizer_id`. +The user can specify different optimizers using the method `BlockDecomposition.specify!`. +In that case `optimizer_id` is the position of the optimizer in the array of optimizers +passed to `specify!`. +By default, the algorihm uses the first optimizer or the default optimizer if no +optimizer has been specified. + +Depending on the type of the optimizer chosen, the algorithm will use one the +three configurations : `moi_params`, `user_params`, or `custom_params`. """ @with_kw struct SolveIpForm <: AbstractOptimizationAlgorithm - time_limit::Int = 600 - deactivate_artificial_vars = true - enforce_integrality = true - get_dual_bound = true - silent = true - max_nb_ip_primal_sols = 50 - log_level = 0 + optimizer_id::Int = 1 + moi_params::MoiOptimize = MoiOptimize() + user_params::UserOptimize = UserOptimize() + custom_params::CustomOptimize = CustomOptimize() end # SolveIpForm does not have child algorithms, therefore get_child_algorithms() is not defined +# Dispatch on the type of the optimizer to return the parameters +_optimizer_params(algo::SolveIpForm, ::MoiOptimizer) = algo.moi_params +_optimizer_params(algo::SolveIpForm, ::UserOptimizer) = algo.user_params +# TODO : custom optimizer +_optimizer_params(::SolveIpForm, ::NoOptimizer) = nothing + +function run!(algo::SolveIpForm, env::Env, form::Formulation, input::OptimizationInput)::OptimizationOutput + opt = getoptimizer(form, algo.optimizer_id) + params = _optimizer_params(algo, opt) + if params !== nothing + return run!(params, env, form, input; optimizer_id = algo.optimizer_id) + end + return error("Cannot optimize formulation with optimizer of type $(typeof(opt)).") +end + +run!(algo::SolveIpForm, env::Env, reform::Reformulation, input::OptimizationInput) = + run!(algo, env, getmaster(reform), input) + +################################################################################ +# Get units usage (depends on the type of the optimizer) +################################################################################ +function get_units_usage(algo::SolveIpForm, form::Formulation) + opt = getoptimizer(form, algo.optimizer_id) + params = _optimizer_params(algo, opt) + if params !== nothing + return get_units_usage(params, form) + end + return error("Cannot get units usage of optimizer of type $(typeof(opt)).") +end + +# get_units_usage of MoiOptimize function get_units_usage( - algo::SolveIpForm, form::Formulation{Duty} + ::MoiOptimize, form::Formulation{Duty} ) where {Duty<:MathProg.AbstractFormDuty} # we use storage units in the read only mode, as all modifications # (deactivating artificial vars and enforcing integrality) @@ -40,23 +125,40 @@ function get_units_usage( return units_usage end -get_units_usage(algo::SolveIpForm, reform::Reformulation) = +get_units_usage(algo::SolveIpForm, reform::Reformulation) = get_units_usage(algo, getmaster(reform)) +# get_units_usage of UserOptimize +function get_units_usage(::UserOptimize, spform::Formulation{DwSp}) + units_usage = Tuple{AbstractModel, UnitType, UnitAccessMode}[] + push!(units_usage, (spform, StaticVarConstrUnit, READ_ONLY)) + return units_usage +end + +# TODO : get_units_usage of CustomOptimize + +################################################################################ +# run! methods (depends on the type of the optimizer) +################################################################################ + function check_if_optimizer_supports_ip(optimizer::MoiOptimizer) return MOI.supports_constraint(optimizer.inner, MOI.SingleVariable, MOI.Integer) end check_if_optimizer_supports_ip(optimizer::UserOptimizer) = false check_if_optimizer_supports_ip(optimizer::NoOptimizer) = false - -function run!(algo::SolveIpForm, env::Env, form::Formulation, input::OptimizationInput)::OptimizationOutput + +# run! of MoiOptimize +function run!( + algo::MoiOptimize, ::Env, form::Formulation, input::OptimizationInput; + optimizer_id::Int = 1 +)::OptimizationOutput result = OptimizationState( form, ip_primal_bound = get_ip_primal_bound(getoptstate(input)), max_length_ip_primal_sols = algo.max_nb_ip_primal_sols ) - ip_supported = check_if_optimizer_supports_ip(getmoioptimizer(form)) + ip_supported = check_if_optimizer_supports_ip(getoptimizer(form, optimizer_id)) if !ip_supported @warn "Optimizer of formulation with id =", getuid(form), " does not support integer variables. Skip SolveIpForm algorithm." @@ -64,7 +166,7 @@ function run!(algo::SolveIpForm, env::Env, form::Formulation, input::Optimizatio return OptimizationOutput(result) end - primal_sols = optimize_ip_form!(algo, getmoioptimizer(form), form, result) + primal_sols = optimize_ip_form!(algo, getoptimizer(form, optimizer_id), form, result) partial_sol = nothing partial_sol_value = 0.0 @@ -73,7 +175,7 @@ function run!(algo::SolveIpForm, env::Env, form::Formulation, input::Optimizatio partial_sol = get_primal_solution(partsolunit, form) partial_sol_value = getvalue(partial_sol) end - + if length(primal_sols) > 0 if partial_sol !== nothing for primal_sol in primal_sols @@ -104,47 +206,8 @@ function run!(algo::SolveIpForm, env::Env, form::Formulation, input::Optimizatio return OptimizationOutput(result) end -run!(algo::SolveIpForm, env::Env, reform::Reformulation, input::OptimizationInput) = - run!(algo, env, getmaster(reform), input) - -function termination_status!(result::OptimizationState, optimizer::MoiOptimizer) - terminationstatus = MOI.get(getinner(optimizer), MOI.TerminationStatus()) - if terminationstatus != MOI.INFEASIBLE && - terminationstatus != MOI.DUAL_INFEASIBLE && - terminationstatus != MOI.INFEASIBLE_OR_UNBOUNDED && - terminationstatus != MOI.OPTIMIZE_NOT_CALLED && - terminationstatus != MOI.INVALID_MODEL && - terminationstatus != MOI.TIME_LIMIT - - setterminationstatus!(result, convert_status(terminationstatus)) - - if MOI.get(getinner(optimizer), MOI.ResultCount()) <= 0 - msg = """ - Termination status = $(terminationstatus) but no results. - Please, open an issue at https://github.com/atoptima/Coluna.jl/issues - """ - error(msg) - end - else - @warn "Solver has no result to show." - setterminationstatus!(result, INFEASIBLE) - end - return -end - -function optimize_with_moi!(optimizer::MoiOptimizer, form::Formulation, result::OptimizationState) - sync_solver!(optimizer, form) - nbvars = MOI.get(form.moioptimizer.inner, MOI.NumberOfVariables()) - if nbvars <= 0 - @warn "No variable in the formulation." - end - MOI.optimize!(getinner(optimizer)) - termination_status!(result, optimizer) - return -end - function optimize_ip_form!( - algo::SolveIpForm, optimizer::MoiOptimizer, form::Formulation, result::OptimizationState + algo::MoiOptimize, optimizer::MoiOptimizer, form::Formulation, result::OptimizationState ) MOI.set(optimizer.inner, MOI.TimeLimitSec(), algo.time_limit) MOI.set(optimizer.inner, MOI.Silent(), algo.silent) @@ -169,3 +232,42 @@ function optimize_ip_form!( end return primal_sols end + +# run! of UserOptimize +function run!( + algo::UserOptimize, ::Env, spform::Formulation{DwSp}, input::OptimizationInput; + optimizer_id::Int = 1 +)::OptimizationOutput + result = OptimizationState( + spform, + ip_primal_bound = get_ip_primal_bound(getoptstate(input)), + max_length_ip_primal_sols = algo.max_nb_ip_primal_sols + ) + + optimizer = getoptimizer(spform, optimizer_id) + cbdata = MathProg.PricingCallbackData(spform, algo.stage) + optimizer.user_oracle(cbdata) + + if length(cbdata.primal_solutions) > 0 + for primal_sol in cbdata.primal_solutions + add_ip_primal_sol!(result, primal_sol) + end + + if algo.stage == 1 # stage 1 is exact by convention + dual_bound = getvalue(get_ip_primal_bound(result)) + set_ip_dual_bound!(result, DualBound(spform, dual_bound)) + setterminationstatus!(result, OPTIMAL) + else + setterminationstatus!(result, OTHER_LIMIT) + end + else + if algo.stage == 1 + setterminationstatus!(result, INFEASIBLE) + else + setterminationstatus!(result, OTHER_LIMIT) + end + end + return OptimizationOutput(result) +end + +# TODO : run! of CustomOptimize \ No newline at end of file diff --git a/src/Algorithm/basic/solvelpform.jl b/src/Algorithm/basic/solvelpform.jl index be4213ba4..2709c18a0 100644 --- a/src/Algorithm/basic/solvelpform.jl +++ b/src/Algorithm/basic/solvelpform.jl @@ -39,6 +39,42 @@ function get_units_usage( return units_usage end +function termination_status!(result::OptimizationState, optimizer::MoiOptimizer) + terminationstatus = MOI.get(getinner(optimizer), MOI.TerminationStatus()) + if terminationstatus != MOI.INFEASIBLE && + terminationstatus != MOI.DUAL_INFEASIBLE && + terminationstatus != MOI.INFEASIBLE_OR_UNBOUNDED && + terminationstatus != MOI.OPTIMIZE_NOT_CALLED && + terminationstatus != MOI.INVALID_MODEL && + terminationstatus != MOI.TIME_LIMIT + + setterminationstatus!(result, convert_status(terminationstatus)) + + if MOI.get(getinner(optimizer), MOI.ResultCount()) <= 0 + msg = """ + Termination status = $(terminationstatus) but no results. + Please, open an issue at https://github.com/atoptima/Coluna.jl/issues + """ + error(msg) + end + else + @warn "Solver has no result to show." + setterminationstatus!(result, INFEASIBLE) + end + return +end + +function optimize_with_moi!(optimizer::MoiOptimizer, form::Formulation, result::OptimizationState) + sync_solver!(optimizer, form) + nbvars = MOI.get(optimizer.inner, MOI.NumberOfVariables()) + if nbvars <= 0 + @warn "No variable in the formulation." + end + MOI.optimize!(getinner(optimizer)) + termination_status!(result, optimizer) + return +end + function optimize_lp_form!(::SolveLpForm, optimizer, ::Formulation, ::OptimizationState) # fallback error("Cannot optimize LP formulation with optimizer of type ", typeof(optimizer), ".") end @@ -46,12 +82,15 @@ end function optimize_lp_form!( algo::SolveLpForm, optimizer::MoiOptimizer, form::Formulation, result::OptimizationState ) - MOI.set(form.moioptimizer.inner, MOI.Silent(), algo.silent) + MOI.set(optimizer.inner, MOI.Silent(), algo.silent) optimize_with_moi!(optimizer, form, result) return end -function run!(algo::SolveLpForm, env::Env, form::Formulation, input::OptimizationInput)::OptimizationOutput +function run!( + algo::SolveLpForm, env::Env, form::Formulation, input::OptimizationInput, + optimizer_id::Int = 1 +)::OptimizationOutput result = OptimizationState(form) TO.@timeit Coluna._to "SolveLpForm" begin @@ -68,7 +107,7 @@ function run!(algo::SolveLpForm, env::Env, form::Formulation, input::Optimizatio partial_sol_val = getvalue(partial_sol) end - optimizer = getmoioptimizer(form) + optimizer = getoptimizer(form, optimizer_id) optimize_lp_form!(algo, optimizer, form, result) primal_sols = get_primal_solutions(form, optimizer) diff --git a/src/Algorithm/colgen.jl b/src/Algorithm/colgen.jl index 35952a9f5..1f1163757 100644 --- a/src/Algorithm/colgen.jl +++ b/src/Algorithm/colgen.jl @@ -1,7 +1,12 @@ """ Coluna.Algorithm.ColumnGeneration( restr_master_solve_alg = SolveLpForm(get_dual_solution = true) - pricing_prob_solve_alg = DefaultPricing(), + pricing_prob_solve_alg = SolveIpForm( + moi_params = MoiOptimize( + deactivate_artificial_vars = false, + enforce_integrality = false + ) + ) essential_cut_gen_alg = CutCallbacks(call_robust_facultative = false) max_nb_iterations::Int = 1000 log_print_frequency::Int = 1 @@ -19,9 +24,15 @@ restricted master and `pricing_prob_solve_alg` to solve the subproblems. """ @with_kw struct ColumnGeneration <: AbstractOptimizationAlgorithm restr_master_solve_alg = SolveLpForm(get_dual_solution=true) + restr_master_optimizer_id = 1 # TODO : pricing problem solver may be different depending on the # pricing subproblem - pricing_prob_solve_alg = DefaultPricing() + pricing_prob_solve_alg = SolveIpForm( + moi_params = MoiOptimize( + deactivate_artificial_vars = false, + enforce_integrality = false + ) + ) essential_cut_gen_alg = CutCallbacks(call_robust_facultative=false) max_nb_iterations::Int64 = 1000 log_print_frequency::Int64 = 1 @@ -45,7 +56,7 @@ function get_child_algorithms(algo::ColumnGeneration, reform::Reformulation) push!(child_algs, (algo.pricing_prob_solve_alg, spform)) end return child_algs -end +end function get_units_usage(algo::ColumnGeneration, reform::Reformulation) units_usage = Tuple{AbstractModel,UnitType,UnitAccessMode}[] @@ -418,9 +429,10 @@ function cleanup_columns(algo::ColumnGeneration, iteration::Int64, master::Formu iteration % 10 != 0 && return cols_with_redcost = Vector{Pair{Variable,Float64}}() + optimizer = getoptimizer(master, algo.restr_master_optimizer_id) for (id, var) in getvars(master) if getduty(id) <= MasterCol && iscuractive(master, var) && isexplicit(master, var) - push!(cols_with_redcost, var => getreducedcost(master, var)) + push!(cols_with_redcost, var => getreducedcost(master, optimizer, var)) end end @@ -611,7 +623,7 @@ function cg_main_loop!( rm_input = OptimizationInput( OptimizationState(masterform, ip_primal_bound=get_ip_primal_bound(cg_optstate)) ) - rm_output = run!(algo.restr_master_solve_alg, env, masterform, rm_input) + rm_output = run!(algo.restr_master_solve_alg, env, masterform, rm_input, algo.restr_master_optimizer_id) end rm_optstate = getoptstate(rm_output) diff --git a/src/Algorithm/conquer.jl b/src/Algorithm/conquer.jl index 6baebd609..24d0ae172 100644 --- a/src/Algorithm/conquer.jl +++ b/src/Algorithm/conquer.jl @@ -69,7 +69,7 @@ end # ParameterisedHeuristic #################################################################### -RestrictedMasterIPHeuristic() = SolveIpForm(get_dual_bound = false) +RestrictedMasterIPHeuristic() = SolveIpForm(moi_params = MoiOptimize(get_dual_bound = false)) struct ParameterisedHeuristic algorithm::AbstractOptimizationAlgorithm @@ -77,7 +77,7 @@ struct ParameterisedHeuristic nonroot_priority::Float64 frequency::Integer max_depth::Integer - name::String + name::String end DefaultRestrictedMasterHeuristic() = diff --git a/src/Algorithm/pricing.jl b/src/Algorithm/pricing.jl deleted file mode 100644 index 1110f0a57..000000000 --- a/src/Algorithm/pricing.jl +++ /dev/null @@ -1,25 +0,0 @@ -@with_kw struct DefaultPricing <: AbstractOptimizationAlgorithm - pricing_callback::PricingCallback = PricingCallback() - solve_ip_form::SolveIpForm = SolveIpForm(deactivate_artificial_vars=false, enforce_integrality=false, log_level=2) - dispatch::Int = 0 # 0 - automatic, 1 - impose pricing callback, 2 - impose pricing by MIP -end - -function get_child_algorithms(algo::DefaultPricing, spform::Formulation{DwSp}) - child_algs = Tuple{AbstractAlgorithm,AbstractModel}[] - algo.dispatch != 2 && push!(child_algs, (algo.pricing_callback, spform)) - algo.dispatch != 1 && push!(child_algs, (algo.solve_ip_form, spform)) - return child_algs -end - -function run!(algo::DefaultPricing, env::Env, spform::Formulation{DwSp}, input::OptimizationInput)::OptimizationOutput - - if algo.dispatch == 1 && !isa(getuseroptimizer(spform), UserOptimizer) - @error string("Pricing callback is imposed but not defined") - end - - if algo.dispatch != 2 && isa(getuseroptimizer(spform), UserOptimizer) - return run!(algo.pricing_callback, env, spform, input) - end - - return run!(algo.solve_ip_form, env, spform, input) -end \ No newline at end of file diff --git a/src/MathProg/MOIinterface.jl b/src/MathProg/MOIinterface.jl index 88c8de7e1..d38829f9c 100644 --- a/src/MathProg/MOIinterface.jl +++ b/src/MathProg/MOIinterface.jl @@ -17,8 +17,7 @@ function set_obj_sense!(optimizer::MoiOptimizer, ::Type{<:MinSense}) return end -function update_bounds_in_optimizer!(form::Formulation, var::Variable) - optimizer = getmoioptimizer(form) +function update_bounds_in_optimizer!(form::Formulation, optimizer::MoiOptimizer, var::Variable) inner = getinner(optimizer) moi_record = getmoirecord(var) moi_kind = getkind(moi_record) @@ -42,8 +41,7 @@ function update_bounds_in_optimizer!(form::Formulation, var::Variable) end end -function update_cost_in_optimizer!(form::Formulation, var::Variable) - optimizer = getmoioptimizer(form) +function update_cost_in_optimizer!(form::Formulation, optimizer::MoiOptimizer, var::Variable) cost = getcurcost(form, var) moi_index = getindex(getmoirecord(var)) MOI.modify( @@ -53,8 +51,7 @@ function update_cost_in_optimizer!(form::Formulation, var::Variable) return end -function update_obj_const_in_optimizer!(form::Formulation) - optimizer = getmoioptimizer(form) +function update_obj_const_in_optimizer!(form::Formulation, optimizer::MoiOptimizer) MOI.modify( getinner(optimizer), MoiObjective(), MOI.ScalarConstantChange{Float64}(getobjconst(form)) @@ -74,8 +71,9 @@ function update_constr_member_in_optimizer!(optimizer::MoiOptimizer, return end -function update_constr_rhs_in_optimizer!(form::Formulation, constr::Constraint) - optimizer = getmoioptimizer(form) +function update_constr_rhs_in_optimizer!( + form::Formulation, optimizer::MoiOptimizer, constr::Constraint +) moi_c_index = getindex(getmoirecord(constr)) rhs = getcurrhs(form, constr) sense = getcursense(form, constr) @@ -83,8 +81,9 @@ function update_constr_rhs_in_optimizer!(form::Formulation, constr::Constraint) return end -function enforce_bounds_in_optimizer!(form::Formulation, var::Variable) - optimizer = getmoioptimizer(form) +function enforce_bounds_in_optimizer!( + form::Formulation, optimizer::MoiOptimizer, var::Variable +) moirecord = getmoirecord(var) moi_bounds = MOI.add_constraint( getinner(optimizer), MOI.SingleVariable(getindex(moirecord)), @@ -94,8 +93,10 @@ function enforce_bounds_in_optimizer!(form::Formulation, var::Variable) return end -function enforce_kind_in_optimizer!(form::Formulation, v::Variable) - inner = getinner(getmoioptimizer(form)) +function enforce_kind_in_optimizer!( + form::Formulation, optimizer::MoiOptimizer, v::Variable +) + inner = getinner(optimizer) kind = getcurkind(form, v) moirecord = getmoirecord(v) moi_kind = getkind(moirecord) @@ -112,24 +113,23 @@ function enforce_kind_in_optimizer!(form::Formulation, v::Variable) return end -function add_to_optimizer!(form::Formulation, var::Variable) - optimizer = getmoioptimizer(form) +function add_to_optimizer!(form::Formulation, optimizer::MoiOptimizer, var::Variable) inner = getinner(optimizer) moirecord = getmoirecord(var) moi_index = MOI.add_variable(inner) setindex!(moirecord, moi_index) - update_cost_in_optimizer!(form, var) - enforce_kind_in_optimizer!(form, var) - enforce_bounds_in_optimizer!(form, var) + update_cost_in_optimizer!(form, optimizer, var) + enforce_kind_in_optimizer!(form, optimizer, var) + enforce_bounds_in_optimizer!(form, optimizer, var) MOI.set(inner, MOI.VariableName(), moi_index, getname(form, var)) return end -function add_to_optimizer!(form::Formulation, constr::Constraint, var_checker::Function) +function add_to_optimizer!( + form::Formulation, optimizer::MoiOptimizer, constr::Constraint, var_checker::Function +) constr_id = getid(constr) - - inner = getinner(getmoioptimizer(form)) - + inner = getinner(optimizer) matrix = getcoefmatrix(form) terms = MOI.ScalarAffineTerm{Float64}[] for (varid, coeff) in @view matrix[constr_id, :] @@ -151,8 +151,18 @@ function add_to_optimizer!(form::Formulation, constr::Constraint, var_checker::F return end -function remove_from_optimizer!(form::Formulation, var::Variable) - inner = getinner(form.moioptimizer) +function remove_from_optimizer!(form::Formulation, optimizer::MoiOptimizer, ids::Set{Id{T}}) where { + T <: AbstractVarConstr} + for id in ids + vc = getelem(form, id) + @logmsg LogLevel(-3) string("Removing varconstr of name ", getname(form, vc)) + remove_from_optimizer!(form, optimizer, vc) + end + return +end + +function remove_from_optimizer!(::Formulation, optimizer::MoiOptimizer, var::Variable) + inner = getinner(optimizer) moirecord = getmoirecord(var) @assert getindex(moirecord).value != -1 MOI.delete(inner, getbounds(moirecord)) @@ -164,10 +174,12 @@ function remove_from_optimizer!(form::Formulation, var::Variable) return end -function remove_from_optimizer!(form::Formulation, constr::Constraint) +function remove_from_optimizer!( + ::Formulation, optimizer::MoiOptimizer, constr::Constraint +) moirecord = getmoirecord(constr) @assert getindex(moirecord).value != -1 - MOI.delete(getinner(form.moioptimizer), getindex(moirecord)) + MOI.delete(getinner(optimizer), getindex(moirecord)) setindex!(moirecord, MoiConstrIndex()) return end @@ -188,7 +200,7 @@ function _getreducedcost(form::Formulation, optimizer, var::Variable) return nothing end -function _getreducedcost(form::Formulation, optimizer::MoiOptimizer, var::Variable) +function getreducedcost(form::Formulation, optimizer::MoiOptimizer, var::Variable) sign = getobjsense(form) == MinSense ? 1.0 : -1.0 inner = getinner(optimizer) if MOI.get(inner, MOI.ResultCount()) < 1 @@ -210,8 +222,7 @@ function _getreducedcost(form::Formulation, optimizer::MoiOptimizer, var::Variab dualval = MOI.get(inner, MOI.ConstraintDual(1), bounds_interval_idx) return sign * dualval end -getreducedcost(form::Formulation, var::Variable) = _getreducedcost(form, getmoioptimizer(form), var) -getreducedcost(form::Formulation, varid::VarId) = _getreducedcost(form, getmoioptimizer(form), getvar(form, varid)) +getreducedcost(form::Formulation, optimizer::MoiOptimizer, varid::VarId) = getreducedcost(form, optimizer, getvar(form, varid)) function get_primal_solutions(form::F, optimizer::MoiOptimizer) where {F <: Formulation} inner = getinner(optimizer) diff --git a/src/MathProg/MathProg.jl b/src/MathProg/MathProg.jl index 38ab4973e..04a5d4135 100644 --- a/src/MathProg/MathProg.jl +++ b/src/MathProg/MathProg.jl @@ -52,7 +52,7 @@ export MaxSense, MinSense, MoiOptimizer, export no_optimizer_builder, set_original_formulation!, getid, getuid, enforce_integrality!, relax_integrality!, - getobjsense, getmoioptimizer, getuseroptimizer, + getobjsense, getoptimizer, getoptimizers, setdualbound!, computereducedcost, update!, @@ -82,8 +82,8 @@ export AbstractFormulation, Formulation, create_formulation!, getreformulation, getdualsolmatrix, getdualsolrhss, setvar!, setconstr!, setprimalsol!, setdualsol!, set_robust_constr_generator!, get_robust_constr_generators, setcol_from_sp_primalsol!, setcut_from_sp_dualsol!, # TODO : merge with setvar! & setconstr - set_objective_sense!, clonevar!, cloneconstr!, clonecoeffs!, initialize_moioptimizer!, - getobjconst, setobjconst! + set_objective_sense!, clonevar!, cloneconstr!, clonecoeffs!, initialize_optimizer!, + push_optimizer!, getobjconst, setobjconst! # Duties of formulations export Original, DwMaster, BendersMaster, DwSp, BendersSp diff --git a/src/MathProg/formulation.jl b/src/MathProg/formulation.jl index 8b9d6707d..9c62e5208 100644 --- a/src/MathProg/formulation.jl +++ b/src/MathProg/formulation.jl @@ -3,14 +3,14 @@ mutable struct Formulation{Duty <: AbstractFormDuty} <: AbstractFormulation var_counter::Int constr_counter::Int parent_formulation::Union{AbstractFormulation, Nothing} # master for sp, reformulation for master - moioptimizer::AbstractOptimizer - useroptimizer::AbstractOptimizer + optimizers::Vector{AbstractOptimizer} manager::FormulationManager obj_sense::Type{<:Coluna.AbstractSense} buffer::FormulationBuffer storage::Storage end + """ `Formulation` stores a mixed-integer linear program. @@ -21,8 +21,8 @@ end obj_sense::Type{<:Coluna.AbstractSense} = MinSense ) - Create a new formulation in the Coluna's environment `env` with duty `duty`, - parent formulation `parent_formulation`, and objective sense `obj_sense`. +Create a new formulation in the Coluna's environment `env` with duty `duty`, +parent formulation `parent_formulation`, and objective sense `obj_sense`. """ function create_formulation!( env::Coluna.Env, @@ -34,9 +34,8 @@ function create_formulation!( error("Maximum number of formulations reached.") end return Formulation{duty}( - env.form_counter += 1, 0, 0, parent_formulation, NoOptimizer(), - NoOptimizer(), FormulationManager(), obj_sense, FormulationBuffer(), - Storage() + env.form_counter += 1, 0, 0, parent_formulation, AbstractOptimizer[], + FormulationManager(), obj_sense, FormulationBuffer(), Storage() ) end @@ -97,8 +96,13 @@ getuid(form::Formulation) = form.uid getobjsense(form::Formulation) = form.obj_sense "Returns the `AbstractOptimizer` of `Formulation` `form`." -getmoioptimizer(form::Formulation) = form.moioptimizer -getuseroptimizer(form::Formulation) = form.useroptimizer +function getoptimizer(form::Formulation, id::Int) + if id <= 0 && id > length(form.optimizers) + return NoOptimizer() + end + return form.optimizers[id] +end +getoptimizers(form::Formulation) = form.optimizers getelem(form::Formulation, id::VarId) = getvar(form, id) getelem(form::Formulation, id::ConstrId) = getconstr(form, id) @@ -117,7 +121,7 @@ _reset_buffer!(form::Formulation) = form.buffer = FormulationBuffer() """ set_matrix_coeff!(form::Formulation, v_id::Id{Variable}, c_id::Id{Constraint}, new_coeff::Float64) -Buffers the matrix modification in `form.buffer` to be sent to `form.moioptimizer` right before next call to optimize!. +Buffers the matrix modification in `form.buffer` to be sent to the optimizers right before next call to optimize!. """ set_matrix_coeff!( form::Formulation, varid::VarId, constrid::ConstrId, new_coeff::Float64 @@ -516,16 +520,6 @@ function set_objective_sense!(form::Formulation, min::Bool) return end -function remove_from_optimizer!(ids::Set{Id{T}}, form::Formulation) where { - T <: AbstractVarConstr} - for id in ids - vc = getelem(form, id) - @logmsg LogLevel(-3) string("Removing varconstr of name ", getname(form, vc)) - remove_from_optimizer!(form, vc) - end - return -end - function computesolvalue(form::Formulation, sol_vec::AbstractDict{Id{Variable}, Float64}) val = sum(getperencost(form, varid) * value for (varid, value) in sol_vec) return val @@ -563,10 +557,10 @@ function constraint_primal(primalsol::PrimalSolution, constrid::ConstrId) return val end -function initialize_moioptimizer!(form::Formulation, builder::Function) +function push_optimizer!(form::Formulation, builder::Function) opt = builder() - form.moioptimizer = opt - _initialize_moioptimizer!(opt, form) + push!(form.optimizers, opt) + initialize_optimizer!(opt, form) return end @@ -646,16 +640,6 @@ function Base.show(io::IO, form::Formulation{Duty}) where {Duty <: AbstractFormD return end -function write_to_LP_file(form::Formulation, filename::String) - optimizer = getmoioptimizer(form) - if isa(optimizer, MoiOptimizer) - src = getinner(optimizer) - dest = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_LP) - MOI.copy_to(dest, src) - MOI.write_to_file(dest, filename) - end -end - # function getspsol(master::Formulation{DwMaster}, col_id::VarId) # !(getduty(col_id) <= MasterCol) && return diff --git a/src/MathProg/optimizerwrappers.jl b/src/MathProg/optimizerwrappers.jl index d7aaac051..a8944417d 100644 --- a/src/MathProg/optimizerwrappers.jl +++ b/src/MathProg/optimizerwrappers.jl @@ -45,35 +45,37 @@ function sync_solver!(optimizer::MoiOptimizer, f::Formulation) # Remove constrs @logmsg LogLevel(-2) string("Removing constraints") - remove_from_optimizer!(buffer.constr_buffer.removed, f) + remove_from_optimizer!(f, optimizer, buffer.constr_buffer.removed) # Remove vars @logmsg LogLevel(-2) string("Removing variables") - remove_from_optimizer!(buffer.var_buffer.removed, f) + remove_from_optimizer!(f, optimizer, buffer.var_buffer.removed) # Add vars for id in buffer.var_buffer.added v = getvar(f, id) @logmsg LogLevel(-4) string("Adding variable ", getname(f, v)) - add_to_optimizer!(f, v) + add_to_optimizer!(f, optimizer, v) end # Add constrs for constr_id in buffer.constr_buffer.added constr = getconstr(f, constr_id) @logmsg LogLevel(-2) string("Adding constraint ", getname(f, constr)) - add_to_optimizer!(f, constr, (f, constr) -> iscuractive(f, constr) && isexplicit(f, constr)) + add_to_optimizer!( + f, optimizer, constr, (f, constr) -> iscuractive(f, constr) && isexplicit(f, constr) + ) end # Update variable costs for id in buffer.changed_cost (id in buffer.var_buffer.added || id in buffer.var_buffer.removed) && continue - update_cost_in_optimizer!(f, getvar(f, id)) + update_cost_in_optimizer!(f, optimizer, getvar(f, id)) end # Update objective constant if buffer.changed_obj_const - update_obj_const_in_optimizer!(f) + update_obj_const_in_optimizer!(f, optimizer) buffer.changed_obj_const = false end @@ -83,7 +85,7 @@ function sync_solver!(optimizer::MoiOptimizer, f::Formulation) @logmsg LogLevel(-4) "Changing bounds of variable " getname(f, id) @logmsg LogLevel(-5) string("New lower bound is ", getcurlb(f, id)) @logmsg LogLevel(-5) string("New upper bound is ", getcurub(f, id)) - update_bounds_in_optimizer!(f, getvar(f, id)) + update_bounds_in_optimizer!(f, optimizer, getvar(f, id)) end # Update variable kind @@ -91,7 +93,7 @@ function sync_solver!(optimizer::MoiOptimizer, f::Formulation) (id in buffer.var_buffer.added || id in buffer.var_buffer.removed) && continue @logmsg LogLevel(-3) "Changing kind of variable " getname(f, id) @logmsg LogLevel(-4) string("New kind is ", getcurkind(f, id)) - enforce_kind_in_optimizer!(f, getvar(f,id)) + enforce_kind_in_optimizer!(f, optimizer, getvar(f,id)) end # Update constraint rhs @@ -99,7 +101,7 @@ function sync_solver!(optimizer::MoiOptimizer, f::Formulation) (id in buffer.constr_buffer.added || id in buffer.constr_buffer.removed) && continue @logmsg LogLevel(-3) "Changing rhs of constraint " getname(f, id) @logmsg LogLevel(-4) string("New rhs is ", getcurrhs(f, id)) - update_constr_rhs_in_optimizer!(f, getconstr(f, id)) + update_constr_rhs_in_optimizer!(f, optimizer, getconstr(f, id)) end # Update matrix @@ -131,11 +133,17 @@ function sync_solver!(optimizer::MoiOptimizer, f::Formulation) end # Initialization of optimizers -function _initialize_moioptimizer!(optimizer::MoiOptimizer, form::Formulation) +function initialize_optimizer!(optimizer::MoiOptimizer, form::Formulation) f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], 0.0) - MOI.set(form.moioptimizer.inner, MoiObjective(), f) - set_obj_sense!(form.moioptimizer, getobjsense(form)) + MOI.set(optimizer.inner, MoiObjective(), f) + set_obj_sense!(optimizer, getobjsense(form)) return end -_initialize_moioptimizer!(optimizer, form::Formulation) = return +initialize_optimizer!(optimizer, form::Formulation) = return +function write_to_LP_file(form::Formulation, optimizer::MoiOptimizer, filename::String) + src = getinner(optimizer) + dest = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_LP) + MOI.copy_to(dest, src) + MOI.write_to_file(dest, filename) +end diff --git a/src/decomposition.jl b/src/decomposition.jl index bb69d0e30..aa1542d45 100644 --- a/src/decomposition.jl +++ b/src/decomposition.jl @@ -425,11 +425,15 @@ function assign_orig_vars_constrs!( clonecoeffs!(origform, destform) end -function getmoioptbuilder(prob::Problem, ann::BD.Annotation) - if BD.getoptimizerbuilder(ann) !== nothing - return () -> MoiOptimizer(BD.getoptimizerbuilder(ann)) +_optimizerbuilder(opt::Function) = () -> UserOptimizer(opt) +_optimizerbuilder(opt::MOI.AbstractOptimizer) = () -> MoiOptimizer(opt) + +function getoptimizerbuilders(prob::Problem, ann::BD.Annotation) + optimizers = BD.getoptimizerbuilders(ann) + if length(optimizers) > 0 + return map(o -> _optimizerbuilder(o), optimizers) end - return prob.default_optimizer_builder + return [prob.default_optimizer_builder] end function buildformulations!( @@ -449,8 +453,8 @@ function buildformulations!( create_side_vars_constrs!(masterform, origform, env, annotations) create_artificial_vars!(masterform, env) closefillmode!(getcoefmatrix(masterform)) - initialize_moioptimizer!(masterform, getmoioptbuilder(prob, ann)) - initialize_moioptimizer!(origform, getmoioptbuilder(prob, ann)) + push_optimizer!.(Ref(masterform), getoptimizerbuilders(prob, ann)) + push_optimizer!.(Ref(origform), getoptimizerbuilders(prob, ann)) return end @@ -468,12 +472,7 @@ function buildformulations!( assign_orig_vars_constrs!(spform, origform, env, annotations, ann) create_side_vars_constrs!(spform, origform, env, annotations) closefillmode!(getcoefmatrix(spform)) - initialize_moioptimizer!(spform, getmoioptbuilder(prob, ann)) - - if BD.getpricingoracle(ann) !== nothing - spform.useroptimizer = UserOptimizer(BD.getpricingoracle(ann)) - end - + push_optimizer!.(Ref(spform), getoptimizerbuilders(prob, ann)) return end @@ -489,7 +488,7 @@ function reformulate!(prob::Problem, annotations::Annotations, env::Env) buildformulations!(prob, reform, env, annotations, reform, root) relax_integrality!(getmaster(reform)) else - initialize_moioptimizer!( + push_optimizer!( prob.original_formulation, prob.default_optimizer_builder ) diff --git a/test/full_instances_tests.jl b/test/full_instances_tests.jl index 2c0ebd8f8..6bbf48175 100644 --- a/test/full_instances_tests.jl +++ b/test/full_instances_tests.jl @@ -271,7 +271,7 @@ function generalized_assignment_tests() try JuMP.optimize!(problem) catch e - @test repr(e) == "ErrorException(\"Cannot optimize LP formulation with optimizer of type Coluna.MathProg.NoOptimizer.\")" + @test e isa ErrorException end end diff --git a/test/pricing_callback_tests.jl b/test/pricing_callback_tests.jl index 3eec28431..de2e442f5 100644 --- a/test/pricing_callback_tests.jl +++ b/test/pricing_callback_tests.jl @@ -10,12 +10,12 @@ function pricing_callback_tests() solver = ClA.TreeSearchAlgorithm( conqueralg = ClA.ColCutGenConquer( stages = [ClA.ColumnGeneration( - pricing_prob_solve_alg = ClA.DefaultPricing( - dispatch=1, pricing_callback = PricingCallback(stage=1) + pricing_prob_solve_alg = ClA.SolveIpForm( + optimizer_id=2, user_params = ClA.UserOptimize(stage=1) )), ClA.ColumnGeneration( - pricing_prob_solve_alg = ClA.DefaultPricing( - dispatch=1, pricing_callback = PricingCallback(stage=2) + pricing_prob_solve_alg = ClA.SolveIpForm( + optimizer_id=2, user_params = ClA.UserOptimize(stage=2) )) ] ) @@ -87,7 +87,7 @@ function pricing_callback_tests() master = BD.getmaster(dec) subproblems = BD.getsubproblems(dec) - BD.specify!.(subproblems, lower_multiplicity = 0, solver = my_pricing_callback) + BD.specify!.(subproblems, lower_multiplicity = 0, solver = [GLPK.Optimizer, my_pricing_callback]) JuMP.optimize!(model) @test nb_exact_calls < 30 # WARNING: this test is necessary to properly test stage 2. diff --git a/test/show_functions_tests.jl b/test/show_functions_tests.jl index ae4eaa08b..0b3eb00bb 100644 --- a/test/show_functions_tests.jl +++ b/test/show_functions_tests.jl @@ -9,5 +9,5 @@ function show_functions_tests() problem, x, dec = CLD.GeneralizedAssignment.model(data, coluna, true) @test occursin("A JuMP Model", repr(problem)) JuMP.optimize!(problem) - @test_nowarn Base.show(problem.moi_backend.inner.re_formulation.master.moioptimizer) + @test_nowarn Base.show(problem.moi_backend.inner.re_formulation.master.optimizers[1]) end diff --git a/test/unit/MathProg/buffer.jl b/test/unit/MathProg/buffer.jl index ed0a893eb..70a0c7240 100644 --- a/test/unit/MathProg/buffer.jl +++ b/test/unit/MathProg/buffer.jl @@ -1,6 +1,6 @@ function buffer_tests() form = create_formulation!(Env(Coluna.Params()), Original) - form.moioptimizer = MoiOptimizer(MOI._instantiate_and_check(GLPK.Optimizer)) + push!(form.optimizers, MoiOptimizer(MOI._instantiate_and_check(GLPK.Optimizer))) var = ClMP.setvar!( form, "var1", ClMP.OriginalVar, cost=2.0, lb=-1.0, ub=1.0, kind=ClMP.Integ, inc_val=4.0 @@ -14,35 +14,35 @@ function buffer_tests() # var `setcurcost!` ClMP.setcurcost!(form, var, 3.0) ClMP.deactivate!(form, var) - ClMP.sync_solver!(getmoioptimizer(form), form) + ClMP.sync_solver!(getoptimizer(form, 1), form) @test ClMP.iscuractive(form, var) == false # var `setcurkind!` ClMP.reset!(form, var) ClMP.setcurkind!(form, var, Integ) ClMP.deactivate!(form, var) - ClMP.sync_solver!(getmoioptimizer(form), form) + ClMP.sync_solver!(getoptimizer(form, 1), form) @test ClMP.iscuractive(form, var) == false # var `setcurlb!` ClMP.reset!(form, var) ClMP.setcurlb!(form, var, 0.0) ClMP.deactivate!(form, var) - ClMP.sync_solver!(getmoioptimizer(form), form) + ClMP.sync_solver!(getoptimizer(form, 1), form) @test ClMP.iscuractive(form, var) == false # var `setcurub!` ClMP.reset!(form, var) ClMP.setcurub!(form, var, 0.0) ClMP.deactivate!(form, var) - ClMP.sync_solver!(getmoioptimizer(form), form) + ClMP.sync_solver!(getoptimizer(form, 1), form) @test ClMP.iscuractive(form, var) == false # constr `setcurrhs!` ClMP.reset!(form, constr) ClMP.setcurrhs!(form, constr, 0.0) ClMP.deactivate!(form, constr) - ClMP.sync_solver!(getmoioptimizer(form), form) + ClMP.sync_solver!(getoptimizer(form, 1), form) @test ClMP.iscuractive(form, constr) == false # `set_matrix_coeff!` @@ -51,7 +51,7 @@ function buffer_tests() ClMP.set_matrix_coeff!(form, getid(var), getid(constr), 2.0) ClMP.deactivate!(form, var) ClMP.deactivate!(form, constr) - ClMP.sync_solver!(getmoioptimizer(form), form) + ClMP.sync_solver!(getoptimizer(form, 1), form) @test ClMP.iscuractive(form, var) == false @test ClMP.iscuractive(form, constr) == false end \ No newline at end of file