diff --git a/src/Algorithm/benders.jl b/src/Algorithm/benders.jl index 416824506..ebd4032bd 100644 --- a/src/Algorithm/benders.jl +++ b/src/Algorithm/benders.jl @@ -1,5 +1,4 @@ @with_kw struct BendersCutGeneration <: AbstractOptimizationAlgorithm - option_use_reduced_cost::Bool = false option_increase_cost_in_hybrid_phase::Bool = false feasibility_tol::Float64 = 1e-5 optimality_tol::Float64 = Coluna.DEF_OPTIMALITY_ATOL @@ -110,15 +109,16 @@ function update_benders_sp_problem!( setcurub!(spform, var, getperenub(spform, var) - master_primal_sol[varid]) end - if algo.option_use_reduced_cost - for (varid, var) in getvars(spform) - iscuractive(spform, varid) || continue - getduty(varid) <= BendSpSlackFirstStageVar || continue - cost = getcurcost(spform, var) - rc = computereducedcost(masterform, varid, master_dual_sol) - setcurcost!(spform, var, rc) - end - end + # TODO : it was untested + # if algo.option_use_reduced_cost + # for (varid, var) in getvars(spform) + # iscuractive(spform, varid) || continue + # getduty(varid) <= BendSpSlackFirstStageVar || continue + # cost = getcurcost(spform, var) + # rc = computereducedcost(masterform, varid, master_dual_sol) + # setcurcost!(spform, var, rc) + # end + # end return false end @@ -190,7 +190,7 @@ function insert_cuts_in_master!( name = string("BC_", getsortuid(dual_sol_id)) kind = Essential duty = MasterBendCutConstr - bc = setcut_from_sp_dualsol!( + setcut_from_sp_dualsol!( masterform, spform, dual_sol_id, @@ -233,7 +233,6 @@ function solve_sp_to_gencut!( benders_sp_primal_bound_contrib = 0.0 benders_sp_lagrangian_bound_contrib = 0.0 - insertion_status = 0 spsol_relaxed = false # Compute target @@ -363,7 +362,7 @@ function solve_sps_to_gencuts!( ### BEGIN LOOP TO BE PARALLELIZED for (spuid, spform) in sps - recorded_sp_dual_solution_ids[spuid] = Vector{ConstrId}() + recorded_sp_dual_solution_ids[spuid] = ConstrId[] gen_status, spsol_relaxed, recorded_dual_solution_ids, benders_sp_primal_bound_contrib, benders_sp_lagrangian_bound_contrib = solve_sp_to_gencut!( algo, env, algdata, masterform, spform, master_primalsol, master_dualsol, @@ -559,7 +558,6 @@ function bend_cutting_plane_main_loop!( setterminationstatus!(bnd_optstate, OPTIMAL) break # loop on master lp solution end - end # loop on master lp solution if !one_spsol_is_a_relaxed_sol diff --git a/src/Algorithm/colgen.jl b/src/Algorithm/colgen.jl index dff0a9949..b7cf6aef9 100644 --- a/src/Algorithm/colgen.jl +++ b/src/Algorithm/colgen.jl @@ -342,7 +342,7 @@ function solve_sp_to_gencol!( sense = getobjsense(masterform) bestsol = get_best_ip_primal_sol(sp_optstate) - if bestsol !== nothing && bestsol.status == FEASIBLE_SOL + if bestsol !== nothing && getstatus(bestsol) == FEASIBLE_SOL spinfo.bestsol = bestsol spinfo.isfeasible = true for sol in get_ip_primal_sols(sp_optstate) @@ -380,7 +380,7 @@ function updatereducedcosts!( reform::Reformulation, redcostshelper::ReducedCostsCalculationHelper, masterdualsol::DualSolution ) redcosts = Dict{VarId,Float64}() - result = redcostshelper.dwsprep_coefmatrix * getsol(masterdualsol) + result = redcostshelper.dwsprep_coefmatrix * masterdualsol.solution.sol for (i, varid) in enumerate(redcostshelper.dwspvarids) redcosts[varid] = redcostshelper.perencosts[i] - get(result, varid, 0.0) end @@ -524,7 +524,7 @@ function update_lagrangian_dual_bound!( end end - valid_lagr_bound = DualBound{S}(puremastvars_contrib + dualsol.bound) + valid_lagr_bound = DualBound{S}(puremastvars_contrib + getbound(dualsol)) for (spuid, spinfo) in spinfos valid_lagr_bound += spinfo.valid_dual_bound_contrib end @@ -533,7 +533,7 @@ function update_lagrangian_dual_bound!( update_lp_dual_bound!(optstate, valid_lagr_bound) if stabilization_is_used(algo) - pseudo_lagr_bound = DualBound{S}(puremastvars_contrib + dualsol.bound) + pseudo_lagr_bound = DualBound{S}(puremastvars_contrib + getbound(dualsol)) for (spuid, spinfo) in spinfos pseudo_lagr_bound += spinfo.pseudo_dual_bound_contrib end @@ -560,9 +560,9 @@ function compute_subgradient_contribution( end end - for (spuid, spinfo) in spinfos + for (_, spinfo) in spinfos iszero(spinfo.ub) && continue - mult = improving_red_cost(spinfo.bestsol.bound, algo, sense) ? spinfo.ub : spinfo.lb + mult = improving_red_cost(getbound(spinfo.bestsol), algo, sense) ? spinfo.ub : spinfo.lb for (sp_var_id, sp_var_val) in spinfo.bestsol for (master_constrid, sp_var_coef) in @view master_coef_matrix[:,sp_var_id] if !(getduty(master_constrid) <= MasterConvexityConstr) @@ -574,13 +574,16 @@ function compute_subgradient_contribution( end end - return DualSolution(master, constrids, constrvals, 0.0, UNKNOWN_SOLUTION_STATUS) + return DualSolution( + master, constrids, constrvals, VarId[], Float64[], ActiveBound[], 0.0, + UNKNOWN_SOLUTION_STATUS + ) end function move_convexity_constrs_dual_values!( spinfos::Dict{FormId,SubprobInfo}, dualsol::DualSolution ) - newbound = dualsol.bound + newbound = getbound(dualsol) for (spuid, spinfo) in spinfos spinfo.lb_dual = dualsol[spinfo.lb_constr_id] spinfo.ub_dual = dualsol[spinfo.ub_constr_id] @@ -594,7 +597,10 @@ function move_convexity_constrs_dual_values!( push!(values, value) end end - return DualSolution(dualsol.model, constrids, values, newbound, FEASIBLE_SOL) + return DualSolution( + getmodel(dualsol), constrids, values, VarId[], Float64[], ActiveBound[], newbound, + FEASIBLE_SOL + ) end function get_pure_master_vars(master::Formulation) @@ -803,8 +809,9 @@ function cg_main_loop!( if nb_new_columns == 0 && !essential_cuts_separated @logmsg LogLevel(0) "No new column generated by the pricing problems." setterminationstatus!(cg_optstate, OTHER_LIMIT) - # if no columns are generated and lp gap is not closed then this col.gen. stage + # If no columns are generated and lp gap is not closed then this col.gen. stage # is a heuristic one, so we do not run phase 1 to save time + # Comment by @guimarqu : It may also be a bug return true, false end if iteration > algo.max_nb_iterations diff --git a/src/Algorithm/colgenstabilization.jl b/src/Algorithm/colgenstabilization.jl index 90fe48f2a..61f63c287 100644 --- a/src/Algorithm/colgenstabilization.jl +++ b/src/Algorithm/colgenstabilization.jl @@ -49,7 +49,7 @@ function init_stab_before_colgen_loop!(unit::ColGenStabilizationUnit) end function componentwisefunction(in_dual_sol::DualSolution, out_dual_sol::DualSolution, f::Function) - form = out_dual_sol.model + form = getmodel(out_dual_sol) constrids = Vector{ConstrId}() constrvals = Vector{Float64}() value::Float64 = 0.0 @@ -100,12 +100,15 @@ function linear_combination(in_dual_sol::DualSolution, out_dual_sol::DualSolutio (x, y) -> coeff * x + (1.0 - coeff) * y ) - form = in_dual_sol.model + form = getmodel(in_dual_sol) bound = 0.0 for (i, constrid) in enumerate(constrids) bound += constrvals[i] * getcurrhs(form, constrid) end - return DualSolution(form, constrids, constrvals, bound, UNKNOWN_FEASIBILITY) + return DualSolution( + form, constrids, constrvals, VarId[], Float64[], ActiveBound[], bound, + UNKNOWN_FEASIBILITY + ) end function update_stab_after_rm_solve!( @@ -148,11 +151,14 @@ function update_alpha_automatically!( smooth_dual_sol::DualSolution{M}, subgradient_contribution::DualSolution{M} ) where {M} - master = lp_dual_sol.model + master = getmodel(lp_dual_sol) # first we calculate the in-sep direction constrids, constrvals = componentwisefunction(smooth_dual_sol, unit.stabcenter, -) - in_sep_direction = DualSolution(master, constrids, constrvals, 0.0, UNKNOWN_FEASIBILITY) + in_sep_direction = DualSolution( + master, constrids, constrvals, VarId[], Float64[], ActiveBound[], 0.0, + UNKNOWN_FEASIBILITY + ) in_sep_dir_norm = norm(in_sep_direction) # we initialize the subgradient with the right-hand-side of all master constraints @@ -166,7 +172,10 @@ function update_alpha_automatically!( push!(constrrhs, getcurrhs(master, constrid)) end end - subgradient = DualSolution(master, constrids, constrrhs, 0.0, UNKNOWN_FEASIBILITY) + subgradient = DualSolution( + master, constrids, constrrhs, VarId[], Float64[], ActiveBound[], 0.0, + UNKNOWN_FEASIBILITY + ) # we calculate the subgradient at the sep point for (constrid, value) in subgradient_contribution diff --git a/src/Algorithm/node.jl b/src/Algorithm/node.jl index d3b9da79f..3eb9230f5 100644 --- a/src/Algorithm/node.jl +++ b/src/Algorithm/node.jl @@ -8,7 +8,7 @@ mutable struct Node depth::Int parent::Union{Nothing, Node} optstate::OptimizationState - #branch::Union{Nothing, Branch} # branch::Id{Constraint} + #branch::Union{Nothing, Branch} # branch::ConstrId branchdescription::String recordids::RecordsVector conquerwasrun::Bool diff --git a/src/Algorithm/utilities/optimizationstate.jl b/src/Algorithm/utilities/optimizationstate.jl index 6055897d4..c5c49e222 100644 --- a/src/Algorithm/utilities/optimizationstate.jl +++ b/src/Algorithm/utilities/optimizationstate.jl @@ -1,6 +1,3 @@ -getvalue(sol::Solution) = ColunaBase.getvalue(sol) -getvalue(bnd::Bound) = ColunaBase.getvalue(bnd) - mutable struct OptimizationState{F<:AbstractFormulation,S<:Coluna.AbstractSense} termination_status::TerminationStatus incumbents::ObjValues{S} @@ -15,7 +12,7 @@ mutable struct OptimizationState{F<:AbstractFormulation,S<:Coluna.AbstractSense} lp_dual_sols::Vector{DualSolution{F}} end -function bestbound!(solutions::Vector{Sol}, max_len::Int, new_sol::Sol) where {Sol<:Solution} +function bestbound!(solutions::Vector{Sol}, max_len::Int, new_sol::Sol) where {Sol<:AbstractSolution} push!(solutions, new_sol) sort!(solutions, rev = true) while length(solutions) > max_len @@ -24,7 +21,7 @@ function bestbound!(solutions::Vector{Sol}, max_len::Int, new_sol::Sol) where {S return end -function set!(solutions::Vector{Sol}, ::Int, new_sol::Sol) where {Sol<:Solution} +function set!(solutions::Vector{Sol}, ::Int, new_sol::Sol) where {Sol<:AbstractSolution} empty!(solutions) push!(solutions, new_sol) return diff --git a/src/ColunaBase/ColunaBase.jl b/src/ColunaBase/ColunaBase.jl index 22f044dd1..38b08f18b 100644 --- a/src/ColunaBase/ColunaBase.jl +++ b/src/ColunaBase/ColunaBase.jl @@ -19,8 +19,8 @@ export AbstractModel, AbstractProblem, AbstractSense, AbstractMinSense, Abstract export NestedEnum, @nestedenum, @exported_nestedenum # solsandbounds.jl -export Bound, Solution, getvalue, isbetter, diff, gap, printbounds, getsol, - getstatus, remove_until_last_point +export Bound, Solution, getvalue, getbound, isbetter, diff, gap, printbounds, + getstatus, remove_until_last_point, getmodel # Statuses export TerminationStatus, SolutionStatus, OPTIMIZE_NOT_CALLED, OPTIMAL, diff --git a/src/ColunaBase/solsandbounds.jl b/src/ColunaBase/solsandbounds.jl index 88e7ad57d..ee99aa012 100644 --- a/src/ColunaBase/solsandbounds.jl +++ b/src/ColunaBase/solsandbounds.jl @@ -214,23 +214,24 @@ function convert_status(coluna_status::SolutionStatus) return MOI.OTHER_RESULT_STATUS end -# Solution +# Basic structure of a solution struct Solution{Model<:AbstractModel,Decision,Value} <: AbstractDict{Decision,Value} model::Model bound::Float64 status::SolutionStatus sol::DynamicSparseArrays.PackedMemoryArray{Decision,Value} - custom_data::Union{Nothing, BlockDecomposition.AbstractCustomData} end """ +Solution is an internal data structure of Coluna and should not be used in +algorithms. See `MathProg.PrimalSolution` & `MathProg.DualSolution` instead. + Solution( model::AbstractModel, decisions::Vector, values::Vector, - solution_values::Float64, - status::SolutionStatus, - [custom_data]::Union{Nothing, BlockDecomposition.AbstractCustomData} + solution_value::Float64, + status::SolutionStatus ) Create a solution to the `model`. Other arguments are: @@ -241,42 +242,34 @@ Create a solution to the `model`. Other arguments are: """ function Solution{Mo,De,Va}( model::Mo, decisions::Vector{De}, values::Vector{Va}, solution_value::Float64, - status::SolutionStatus, custom_data::Union{Nothing, BlockDecomposition.AbstractCustomData} = nothing -) where {Mo<:AbstractModel,De,Va} + status::SolutionStatus +) where {Mo<:AbstractModel,De,Va,T} sol = DynamicSparseArrays.dynamicsparsevec(decisions, values) - return Solution(model, solution_value, status, sol, custom_data) + return Solution(model, solution_value, status, sol) end -""" - getsol(solution) - -Return the dynamic sparse vector that describes `solution`. -""" -getsol(s::Solution) = s.sol +"Return the model of a solution." +getmodel(s::Solution) = s.model -""" - getvalue(solution) -> Float64 +"Return the value (as a Bound) of `solution`" +getbound(s::Solution) = s.bound -Return the value of `solution`. -""" +"Return the value of `solution`." getvalue(s::Solution) = float(s.bound) -""" - getstatus(solution) -> SolutionStatus - -Return the solution status of `solution`. -""" +"Return the solution status of `solution`." getstatus(s::Solution) = s.status Base.iterate(s::Solution) = iterate(s.sol) Base.iterate(s::Solution, state) = iterate(s.sol, state) Base.length(s::Solution) = length(s.sol) -Base.get(s::Solution{Mo,De,Va}, id::De, default) where {Mo,De,Va} = s.sol[id] # TODO : REMOVE +Base.get(s::Solution{Mo,De,Va}, id::De, default) where {Mo,De,Va} = s.sol[id] Base.getindex(s::Solution{Mo,De,Va}, id::De) where {Mo,De,Va} = Base.getindex(s.sol, id) Base.setindex!(s::Solution{Mo,De,Va}, val::Va, id::De) where {Mo,De,Va} = s.sol[id] = val +# TODO : remove when refactoring Benders function Base.filter(f::Function, s::S) where {S <: Solution} - return S(s.model, s.bound, s.status, filter(f, s.sol), s.custom_data) + return S(s.model, s.bound, s.status, filter(f, s.sol)) end function Base.in(p::Tuple{De,Va}, a::Solution{Mo,De,Va}, valcmp=(==)) where {Mo,De,Va} diff --git a/src/MOIcallbacks.jl b/src/MOIcallbacks.jl index adb3831ac..03ed2b89c 100644 --- a/src/MOIcallbacks.jl +++ b/src/MOIcallbacks.jl @@ -34,7 +34,10 @@ function MOI.submit( push!(values, 1.0) solval += getcurcost(form, setup_var_id) - sol = PrimalSolution(form, colunavarids, values, solval, FEASIBLE_SOL, custom_data) + sol = PrimalSolution( + form, colunavarids, values, solval, FEASIBLE_SOL; + custom_data = custom_data + ) push!(cb.callback_data.primal_solutions, sol) return end diff --git a/src/MOIwrapper.jl b/src/MOIwrapper.jl index bef4a7a36..05f279457 100644 --- a/src/MOIwrapper.jl +++ b/src/MOIwrapper.jl @@ -14,19 +14,42 @@ const SupportedConstrSets = Union{ } @enum(ObjectiveType, SINGLE_VARIABLE, SCALAR_AFFINE) + +# Helper for SingleVariable constraints +struct BoundConstraints + varid::VarId + lower::Union{Nothing,SingleVarConstraint} + upper::Union{Nothing,SingleVarConstraint} + eq::Union{Nothing,SingleVarConstraint} +end + +setname!(bc, set_type, name) = nothing # Fallback +setname!(bc, ::Type{<:MOI.ZeroOne}, name) = bc.lower.name = bc.upper.name = name +setname!(bc, ::Type{<:MOI.GreaterThan}, name) = bc.lower.name = name +setname!(bc, ::Type{<:MOI.LessThan}, name) = bc.upper.name = name +setname!(bc, ::Type{<:MOI.EqualTo}, name) = bc.eq.name = name +setname!(bc, ::Type{<:MOI.Interval}, name) = bc.lower.name = bc.upper.name = name + +setrhs!(bc, s::MOI.GreaterThan) = bc.lower.perendata.rhs = bc.lower.curdata.rhs = s.lower +setrhs!(bc, s::MOI.LessThan) = bc.upper.perendata.rhs = bc.upper.curdata.rhs = s.upper +setrhs!(bc, s::MOI.EqualTo) = bc.eq.perendata.rhs = bc.eq.curdata.rhs = s.value + +function setrhs!(bc, s::MOI.Interval) + bc.lower.perendata.rhs = bc.lower.curdata.rhs = s.lower + bc.upper.perendata.rhs = bc.upper.curdata.rhs = s.upper + return +end + mutable struct Optimizer <: MOI.AbstractOptimizer env::Env inner::Problem objective_type::ObjectiveType annotations::Annotations - #varmap::Dict{MOI.VariableIndex,VarId} # For the user to get VariablePrimal vars::CleverDicts.CleverDict{MOI.VariableIndex, Variable} - #varids::CleverDicts.CleverDict{MOI.VariableIndex, VarId} moi_varids::Dict{VarId, MOI.VariableIndex} names_to_vars::Dict{String, MOI.VariableIndex} constrs::Dict{MOI.ConstraintIndex, Constraint} - constrs_on_single_var_to_vars::Dict{MOI.ConstraintIndex, VarId} - constrs_on_single_var_to_names::Dict{MOI.ConstraintIndex, String} + constrs_on_single_var::Dict{MOI.ConstraintIndex, BoundConstraints} names_to_constrs::Dict{String, MOI.ConstraintIndex} result::OptimizationState disagg_result::Union{Nothing, OptimizationState} @@ -40,12 +63,10 @@ mutable struct Optimizer <: MOI.AbstractOptimizer model.inner = Problem(model.env) model.annotations = Annotations() model.vars = CleverDicts.CleverDict{MOI.VariableIndex, Variable}() - #model.varids = CleverDicts.CleverDict{MOI.VariableIndex, VarId}() # TODO : check if necessary to have two dicts for variables model.moi_varids = Dict{VarId, MOI.VariableIndex}() model.names_to_vars = Dict{String, MOI.VariableIndex}() - model.constrs = Dict{MOI.ConstraintIndex, Union{Constraint, Nothing}}() - model.constrs_on_single_var_to_vars = Dict{MOI.ConstraintIndex, VarId}() - model.constrs_on_single_var_to_names = Dict{MOI.ConstraintIndex, String}() + model.constrs = Dict{MOI.ConstraintIndex, Constraint}() + model.constrs_on_single_var = Dict{MOI.ConstraintIndex, BoundConstraints}() model.names_to_constrs = Dict{String, MOI.ConstraintIndex}() model.result = OptimizationState(get_optimization_target(model.inner)) model.disagg_result = nothing @@ -55,7 +76,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer end end -MOI.Utilities.supports_default_copy_to(::Coluna.Optimizer, ::Bool) = true +MOI.Utilities.supports_default_copy_to(::Optimizer, ::Bool) = true MOI.supports(::Optimizer, ::MOI.VariableName, ::Type{MOI.VariableIndex}) = true MOI.supports(::Optimizer, ::MOI.ConstraintName, ::Type{<:MOI.ConstraintIndex}) = true MOI.supports_constraint(::Optimizer, ::Type{<:SupportedConstrFunc}, ::Type{<:SupportedConstrSets}) = true @@ -96,7 +117,7 @@ function _get_orig_varid_in_form( return getid(getvar(form, origid)) end -MOI.get(optimizer::Coluna.Optimizer, ::MOI.SolverName) = "Coluna" +MOI.get(optimizer::Optimizer, ::MOI.SolverName) = "Coluna" function MOI.optimize!(optimizer::Optimizer) optimizer.result, optimizer.disagg_result = optimize!( @@ -105,14 +126,14 @@ function MOI.optimize!(optimizer::Optimizer) return end -function MOI.copy_to(dest::Coluna.Optimizer, src::MOI.ModelLike; kwargs...) +function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kwargs...) return MOI.Utilities.automatic_copy_to(dest, src; kwargs...) end ############################################################################################ # Add variables ############################################################################################ -function MOI.add_variable(model::Coluna.Optimizer) +function MOI.add_variable(model::Optimizer) orig_form = get_original_formulation(model.inner) var = setvar!(orig_form, "v", OriginalVar) index = CleverDicts.add_item(model.vars, var) @@ -125,70 +146,83 @@ end ############################################################################################ # Add constraint ############################################################################################ -function _constraint_on_variable!(var::Variable, ::MOI.Integer) - # set perene data - var.perendata.kind = Integ - var.curdata.kind = Integ +function _constraint_on_variable!( + optimizer, form::Formulation, constrid, var::Variable, ::MOI.Integer +) + setperenkind!(form, var, Integ) + optimizer.constrs_on_single_var[constrid] = BoundConstraints(getid(var), nothing, nothing, nothing) return end -function _constraint_on_variable!(var::Variable, ::MOI.ZeroOne) - # set perene data - var.perendata.kind = Binary - var.curdata.kind = Binary - var.perendata.lb = max(0.0, var.perendata.lb) - var.curdata.lb = max(0.0, var.curdata.lb) - var.perendata.ub = min(1.0, var.perendata.ub) - var.curdata.ub = min(1.0, var.curdata.ub) +function _constraint_on_variable!( + optimizer, form::Formulation, constrid, var::Variable, ::MOI.ZeroOne +) + setperenkind!(form, var, Binary) + constr1 = setsinglevarconstr!( + form, "lb", getid(var), OriginalConstr; sense = Greater, rhs = 0.0 + ) + constr2 = setsinglevarconstr!( + form, "ub", getid(var), OriginalConstr; sense = Less, rhs = 1.0 + ) + optimizer.constrs_on_single_var[constrid] = BoundConstraints(getid(var), constr1, constr2, nothing) return end -function _constraint_on_variable!(var::Variable, set::MOI.GreaterThan{Float64}) - # set perene data - var.perendata.lb = max(set.lower, var.perendata.lb) - var.curdata.lb = max(set.lower, var.perendata.lb) +function _constraint_on_variable!( + optimizer, form::Formulation, constrid, var::Variable, set::MOI.GreaterThan{Float64} +) + constr = setsinglevarconstr!( + form, "lb", getid(var), OriginalConstr; sense = Greater, rhs = set.lower + ) + optimizer.constrs_on_single_var[constrid] = BoundConstraints(getid(var), constr, nothing, nothing) return end -function _constraint_on_variable!(var::Variable, set::MOI.LessThan{Float64}) - # set perene data - var.perendata.ub = min(set.upper, var.perendata.ub) - var.curdata.ub = min(set.upper, var.curdata.ub) +function _constraint_on_variable!( + optimizer, form::Formulation, constrid, var::Variable, set::MOI.LessThan{Float64} +) + constr = setsinglevarconstr!( + form, "ub", getid(var), OriginalConstr; sense = Less, rhs = set.upper + ) + optimizer.constrs_on_single_var[constrid] = BoundConstraints(getid(var), nothing, constr, nothing) return end -function _constraint_on_variable!(var::Variable, set::MOI.EqualTo{Float64}) - # set perene data - var.perendata.lb = max(set.value, var.perendata.lb) - var.curdata.lb = max(set.value, var.curdata.lb) - var.perendata.ub = min(set.value, var.perendata.ub) - var.curdata.ub = min(set.value, var.curdata.ub) +function _constraint_on_variable!( + optimizer, form::Formulation, constrid, var::Variable, set::MOI.EqualTo{Float64} +) + constr = setsinglevarconstr!( + form, "eq", getid(var), OriginalConstr; sense = Equal, rhs = set.value + ) + optimizer.constrs_on_single_var[constrid] = BoundConstraint(getid(var), nothing, nothing, constr) return end -function _constraint_on_variable!(var::Variable, set::MOI.Interval{Float64}) - # set perene data - var.perendata.lb = max(set.lower, var.perendata.lb) - var.curdata.lb = max(set.lower, var.curdata.lb) - var.perendata.ub = min(set.upper, var.perendata.ub) - var.curdata.ub = min(set.upper, var.curdata.ub) +function _constraint_on_variable!( + optimizer, form::Formulation, constrid, var::Variable, set::MOI.Interval{Float64} +) + constr1 = setsinglevarconstr!( + form, "lb", getid(var), OriginalConstr; sense = Greater, rhs = set.lower + ) + constr2 = setsinglevarconstr!( + form, "ub", getid(var), OriginalConstr; sense = Less, rhs = set.upper + ) + optimizer.constrs_on_single_var[constrid] = BoundConstraints(geid(var), constr1, constr2, nothing) return end function MOI.add_constraint( - model::Coluna.Optimizer, func::MOI.SingleVariable, set::S + model::Optimizer, func::MOI.SingleVariable, set::S ) where {S<:SupportedVarSets} - orig_form = get_original_formulation(model.inner) + origform = get_original_formulation(model.inner) var = model.vars[func.variable] - _constraint_on_variable!(var, set) constrid = MOI.ConstraintIndex{MOI.SingleVariable, S}(func.variable.value) - model.constrs_on_single_var_to_names[constrid] = "" - model.constrs_on_single_var_to_vars[constrid] = getid(var) + _constraint_on_variable!(model, origform, constrid, var, set) return constrid end function MOI.add_constraint( - model::Coluna.Optimizer, func::MOI.ScalarAffineFunction{Float64}, set::S + model::Optimizer, func::MOI.ScalarAffineFunction{Float64}, set::S ) where {S<:SupportedConstrSets} orig_form = get_original_formulation(model.inner) members = Dict{VarId, Float64}() @@ -212,18 +246,21 @@ end ############################################################################################ # Delete and modify variable ############################################################################################ -function MOI.delete(model::Coluna.Optimizer, vi::MOI.VariableIndex) +function MOI.delete(model::Optimizer, vi::MOI.VariableIndex) + MOI.throw_if_not_valid(model, vi) MOI.modify(model, MoiObjective(), MOI.ScalarCoefficientChange(vi, 0.0)) - for (ci, _) in model.constrs_on_single_var_to_vars - if ci.value == vi.value - MOI.delete(model, ci) - break - end - end for (ci, _) in model.constrs MOI.modify(model, ci, MOI.ScalarCoefficientChange(vi, 0.0)) end varid = getid(model.vars[vi]) + for (ci, constrs) in model.constrs_on_single_var + for constr in [constrs.lower, constrs.upper, constrs.eq] + if constr !== nothing && constr.varid == varid + MOI.delete(model, ci) + break + end + end + end delete!(get_original_formulation(model.inner), varid) delete!(model.moi_varids, varid) delete!(model.vars, vi) @@ -232,7 +269,7 @@ function MOI.delete(model::Coluna.Optimizer, vi::MOI.VariableIndex) end function MOI.modify( - model::Coluna.Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, + model::Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, change::MathOptInterface.ScalarCoefficientChange{Float64} ) setperencost!( @@ -244,33 +281,58 @@ end ############################################################################################ # Delete and modify constraint ############################################################################################ -# issue #583 -# function MOI.delete( -# model::Coluna.Optimizer, ci::MOI.ConstraintIndex{F,S} -# ) where {F<:MOI.SingleVariable,S} -# return -# end +function MOI.delete( + model::Optimizer, ci::MOI.ConstraintIndex{F,S} +) where {F<:MOI.SingleVariable,S} + MOI.throw_if_not_valid(model, ci) + origform = get_original_formulation(model.inner) + constrs = model.constrs_on_single_var[ci] + if constrs.lower !== nothing + delete!(origform, getid(constrs.lower)) + end + if constrs.upper !== nothing + delete!(origform, getid(constrs.upper)) + end + if constrs.eq !== nothing + delete!(origform, getid(constrs.eq)) + end + delete!(model.constrs_on_single_var, ci) + return +end function MOI.delete( - model::Coluna.Optimizer, ci::MOI.ConstraintIndex{F,S} + model::Optimizer, ci::MOI.ConstraintIndex{F,S} ) where {F<:MOI.ScalarAffineFunction{Float64},S} - delete!(get_original_formulation(model.inner), getid(model.constrs[ci])) + MOI.throw_if_not_valid(model, ci) + constrid = getid(model.constrs[ci]) + orig_form = get_original_formulation(model.inner) + coefmatrix = getcoefmatrix(orig_form) + varids = VarId[] + for (varid, _) in @view coefmatrix[constrid, :] + push!(varids, varid) + end + for varid in varids + coefmatrix[constrid, varid] = 0.0 + end + delete!(orig_form, constrid) delete!(model.constrs, ci) return end function MOI.modify( - model::Coluna.Optimizer, ci::MOI.ConstraintIndex{F,S}, + model::Optimizer, ci::MOI.ConstraintIndex{F,S}, change::MOI.ScalarConstantChange{Float64} ) where {F<:MOI.ScalarAffineFunction{Float64},S} + MOI.throw_if_not_valid(model, ci) setperenrhs!(get_original_formulation(model.inner), model.constrs[ci], change.new_constant) return end function MOI.modify( - model::Coluna.Optimizer, ci::MOI.ConstraintIndex{F,S}, + model::Optimizer, ci::MOI.ConstraintIndex{F,S}, change::MOI.ScalarCoefficientChange{Float64} ) where {F<:MOI.ScalarAffineFunction{Float64},S} + MOI.throw_if_not_valid(model, ci) varid = getid(model.vars[change.variable]) constrid = getid(model.constrs[ci]) getcoefmatrix(get_original_formulation(model.inner))[constrid, varid] = change.new_coefficient @@ -280,11 +342,11 @@ end ############################################################################################ # Get variables ############################################################################################ -function MOI.get(model::Coluna.Optimizer, ::Type{MOI.VariableIndex}, name::String) +function MOI.get(model::Optimizer, ::Type{MOI.VariableIndex}, name::String) return get(model.names_to_vars, name, nothing) end -function MOI.get(model::Coluna.Optimizer, ::MOI.ListOfVariableIndices) +function MOI.get(model::Optimizer, ::MOI.ListOfVariableIndices) indices = Vector{MathOptInterface.VariableIndex}() for (_, value) in model.moi_varids push!(indices, value) @@ -304,14 +366,14 @@ function _moi_bounds_type(lb, ub) end function MOI.get( - model::Coluna.Optimizer, C::Type{MOI.ConstraintIndex{F,S}}, name::String + model::Optimizer, C::Type{MOI.ConstraintIndex{F,S}}, name::String ) where {F,S} index = get(model.names_to_constrs, name, nothing) typeof(index) == C && return index return nothing end -function MOI.get(model::Coluna.Optimizer, ::MOI.ListOfConstraints) +function MOI.get(model::Optimizer, ::MOI.ListOfConstraints) orig_form = get_original_formulation(model.inner) constraints = Set{Tuple{DataType, DataType}}() for (id, var) in model.vars @@ -344,7 +406,7 @@ function _add_constraint!( end function MOI.get( - model::Coluna.Optimizer, ::MOI.ListOfConstraintIndices{F, S} + model::Optimizer, ::MOI.ListOfConstraintIndices{F, S} ) where {F<:MOI.ScalarAffineFunction{Float64}, S} indices = MOI.ConstraintIndex{F,S}[] for (id, constr) in model.constrs @@ -354,10 +416,10 @@ function MOI.get( end function MOI.get( - model::Coluna.Optimizer, ::MOI.ListOfConstraintIndices{F, S} + model::Optimizer, ::MOI.ListOfConstraintIndices{F, S} ) where {F<:MOI.SingleVariable, S} indices = MOI.ConstraintIndex{F,S}[] - for (id, _) in model.constrs_on_single_var_to_vars + for (id, _) in model.constrs_on_single_var if S == typeof(MOI.get(model, MOI.ConstraintSet(), id)) push!(indices, id) end @@ -366,7 +428,7 @@ function MOI.get( end function MOI.get( - model::Coluna.Optimizer, ::MOI.ConstraintFunction, index::MOI.ConstraintIndex{F,S} + model::Optimizer, ::MOI.ConstraintFunction, index::MOI.ConstraintIndex{F,S} ) where {F<:MOI.ScalarAffineFunction{Float64}, S} orig_form = get_original_formulation(model.inner) constrid = getid(model.constrs[index]) @@ -378,13 +440,13 @@ function MOI.get( end function MOI.get( - model::Coluna.Optimizer, ::MOI.ConstraintFunction, index::MOI.ConstraintIndex{F,S} + model::Optimizer, ::MOI.ConstraintFunction, index::MOI.ConstraintIndex{F,S} ) where {F<:MOI.SingleVariable, S} return MOI.SingleVariable(MOI.VariableIndex(index.value)) end function MOI.get( - model::Coluna.Optimizer, ::MOI.ConstraintSet, index::MOI.ConstraintIndex{F,S} + model::Optimizer, ::MOI.ConstraintSet, index::MOI.ConstraintIndex{F,S} ) where {F<:MOI.ScalarAffineFunction{Float64},S} orig_form = get_original_formulation(model.inner) rhs = getperenrhs(orig_form, model.constrs[index]) @@ -392,7 +454,7 @@ function MOI.get( end function MOI.get( - model::Coluna.Optimizer, ::MOI.ConstraintSet, + model::Optimizer, ::MOI.ConstraintSet, index::MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{Float64}} ) orig_form = get_original_formulation(model.inner) @@ -401,18 +463,20 @@ function MOI.get( end function MOI.get( - model::Coluna.Optimizer, ::MOI.ConstraintSet, + model::Optimizer, ::MOI.ConstraintSet, index::MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}} ) + MOI.throw_if_not_valid(model, index) orig_form = get_original_formulation(model.inner) ub = getperenub(orig_form, model.vars[MOI.VariableIndex(index.value)]) return MOI.LessThan(ub) end function MOI.get( - model::Coluna.Optimizer, ::MOI.ConstraintSet, + model::Optimizer, ::MOI.ConstraintSet, index::MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{Float64}} ) + MOI.throw_if_not_valid(model, index) orig_form = get_original_formulation(model.inner) lb = getperenlb(orig_form, model.vars[MOI.VariableIndex(index.value)]) ub = getperenub(orig_form, model.vars[MOI.VariableIndex(index.value)]) @@ -421,9 +485,10 @@ function MOI.get( end function MOI.get( - model::Coluna.Optimizer, ::MOI.ConstraintSet, + model::Optimizer, ::MOI.ConstraintSet, index::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Interval{Float64}} ) + MOI.throw_if_not_valid(model, index) orig_form = get_original_formulation(model.inner) lb = getperenlb(orig_form, model.vars[MOI.VariableIndex(index.value)]) ub = getperenub(orig_form, model.vars[MOI.VariableIndex(index.value)]) @@ -431,20 +496,20 @@ function MOI.get( end function MOI.get( - model::Coluna.Optimizer, ::MOI.ConstraintSet, + model::Optimizer, ::MOI.ConstraintSet, index::MOI.ConstraintIndex{MOI.SingleVariable, MOI.ZeroOne} ) return MOI.ZeroOne() end function MOI.get( - model::Coluna.Optimizer, ::MOI.ConstraintSet, + model::Optimizer, ::MOI.ConstraintSet, index::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Integer} ) return MOI.Integer() end -function MOI.get(model::Coluna.Optimizer, ::Type{MOI.ConstraintIndex}, name::String) +function MOI.get(model::Optimizer, ::Type{MOI.ConstraintIndex}, name::String) return get(model.names_to_constrs, name, nothing) end @@ -452,7 +517,7 @@ end # Attributes of variables ############################################################################################ function MOI.set( - model::Coluna.Optimizer, ::BD.VariableDecomposition, varid::MOI.VariableIndex, + model::Optimizer, ::BD.VariableDecomposition, varid::MOI.VariableIndex, annotation::BD.Annotation ) store!(model.annotations, annotation, model.vars[varid]) @@ -460,8 +525,9 @@ function MOI.set( end function MOI.set( - model::Coluna.Optimizer, ::MOI.VariableName, varid::MOI.VariableIndex, name::String + model::Optimizer, ::MOI.VariableName, varid::MOI.VariableIndex, name::String ) + MOI.throw_if_not_valid(model, varid) var = model.vars[varid] # TODO : rm set perene name var.name = name @@ -470,19 +536,19 @@ function MOI.set( end function MOI.set( - model::Coluna.Optimizer, ::BD.VarBranchingPriority, varid::MOI.VariableIndex, branching_priority::Int + model::Optimizer, ::BD.VarBranchingPriority, varid::MOI.VariableIndex, branching_priority::Int ) var = model.vars[varid] var.branching_priority = Float64(branching_priority) return end -function MOI.get(model::Coluna.Optimizer, ::MOI.VariableName, index::MOI.VariableIndex) +function MOI.get(model::Optimizer, ::MOI.VariableName, index::MOI.VariableIndex) orig_form = get_original_formulation(model.inner) return getname(orig_form, model.vars[index]) end -function MOI.get(model::Coluna.Optimizer, ::BD.VarBranchingPriority, varid::MOI.VariableIndex) +function MOI.get(model::Optimizer, ::BD.VarBranchingPriority, varid::MOI.VariableIndex) var = model.vars[varid] return var.branching_priority end @@ -505,7 +571,7 @@ function MOI.set( end function MOI.set( - model::Coluna.Optimizer, ::BD.ConstraintDecomposition, constrid::MOI.ConstraintIndex, + model::Optimizer, ::BD.ConstraintDecomposition, constrid::MOI.ConstraintIndex, annotation::BD.Annotation ) constr = get(model.constrs, constrid, nothing) @@ -516,7 +582,7 @@ function MOI.set( end function MOI.set( - model::Coluna.Optimizer, ::MOI.ConstraintName, constrid::MOI.ConstraintIndex{F,S}, name::String + model::Optimizer, ::MOI.ConstraintName, constrid::MOI.ConstraintIndex{F,S}, name::String ) where {F<:MOI.ScalarAffineFunction,S} MOI.throw_if_not_valid(model, constrid) constr = model.constrs[constrid] @@ -527,17 +593,17 @@ function MOI.set( end function MOI.set( - model::Coluna.Optimizer, ::MOI.ConstraintName, constrid::MOI.ConstraintIndex{F,S}, name::String + model::Optimizer, ::MOI.ConstraintName, constrid::MOI.ConstraintIndex{F,S}, name::String ) where {F<:MOI.SingleVariable,S} MOI.throw_if_not_valid(model, constrid) - model.constrs_on_single_var_to_names[constrid] = name + setname!(model.constrs_on_single_var[constrid], S, name) model.names_to_constrs[name] = constrid return end function MOI.set( model::Coluna.Optimizer, ::MOI.ConstraintSet, constrid::MOI.ConstraintIndex{F,S}, set::S -) where {F,S<:SupportedConstrSets} +) where {F<:SupportedConstrFunc,S<:SupportedConstrSets} MOI.throw_if_not_valid(model, constrid) origform = get_original_formulation(model.inner) constr = model.constrs[constrid] @@ -546,7 +612,16 @@ function MOI.set( return end -function MOI.get(model::Coluna.Optimizer, ::MOI.ConstraintName, constrid::MOI.ConstraintIndex) +function MOI.set( + model::Coluna.Optimizer, ::MOI.ConstraintSet, constrid::MOI.ConstraintIndex{F,S}, set::S +) where {F<:MOI.SingleVariable,S<:SupportedConstrSets} + MOI.throw_if_not_valid(model, constrid) + constrs = model.constrs_on_single_var[constrid] + setrhs!(constrs, set) + return +end + +function MOI.get(model::Optimizer, ::MOI.ConstraintName, constrid::MOI.ConstraintIndex) MOI.throw_if_not_valid(model, constrid) orig_form = get_original_formulation(model.inner) constr = get(model.constrs, constrid, nothing) @@ -563,7 +638,7 @@ end ############################################################################################ # Objective ############################################################################################ -function MOI.set(model::Coluna.Optimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense) +function MOI.set(model::Optimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense) orig_form = get_original_formulation(model.inner) if sense == MOI.MIN_SENSE model.feasibility_sense = false @@ -578,14 +653,14 @@ function MOI.set(model::Coluna.Optimizer, ::MOI.ObjectiveSense, sense::MOI.Optim return end -function MOI.get(model::Coluna.Optimizer, ::MOI.ObjectiveSense) +function MOI.get(model::Optimizer, ::MOI.ObjectiveSense) sense = getobjsense(get_original_formulation(model.inner)) model.feasibility_sense && return MOI.FEASIBILITY_SENSE sense == MaxSense && return MOI.MAX_SENSE return MOI.MIN_SENSE end -function MOI.get(model::Coluna.Optimizer, ::MOI.ObjectiveFunctionType) +function MOI.get(model::Optimizer, ::MOI.ObjectiveFunctionType) if model.objective_type == SINGLE_VARIABLE return MOI.SingleVariable end @@ -594,7 +669,7 @@ function MOI.get(model::Coluna.Optimizer, ::MOI.ObjectiveFunctionType) end function MOI.set( - model::Coluna.Optimizer, ::MOI.ObjectiveFunction{F}, func::F + model::Optimizer, ::MOI.ObjectiveFunction{F}, func::F ) where {F<:MOI.ScalarAffineFunction{Float64}} model.objective_type = SCALAR_AFFINE origform = get_original_formulation(model.inner) @@ -616,19 +691,16 @@ function MOI.set( end function MOI.set( - model::Coluna.Optimizer, ::MOI.ObjectiveFunction{MOI.SingleVariable}, + model::Optimizer, ::MOI.ObjectiveFunction{MOI.SingleVariable}, func::MOI.SingleVariable ) model.objective_type = SINGLE_VARIABLE - var = model.vars[func.variable] - # TODO : rm set perene cost - var.perendata.cost = 1.0 - var.curdata.cost = 1.0 + setperencost!(get_original_formulation(model.inner), model.vars[func.variable], 1.0) return end function MOI.get( - model::Coluna.Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}} + model::Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}} ) @assert model.objective_type == SCALAR_AFFINE orig_form = get_original_formulation(model.inner) @@ -643,7 +715,7 @@ function MOI.get( end function MOI.get( - model::Coluna.Optimizer, ::MOI.ObjectiveFunction{MOI.SingleVariable} + model::Optimizer, ::MOI.ObjectiveFunction{MOI.SingleVariable} ) @assert model.objective_type == SINGLE_VARIABLE orig_form = get_original_formulation(model.inner) @@ -659,29 +731,29 @@ end ############################################################################################ # Attributes of model ############################################################################################ -function MOI.set(model::Coluna.Optimizer, ::BD.DecompositionTree, tree::BD.Tree) +function MOI.set(model::Optimizer, ::BD.DecompositionTree, tree::BD.Tree) model.annotations.tree = tree return end -function MOI.set(model::Coluna.Optimizer, ::BD.ObjectiveDualBound, db) +function MOI.set(model::Optimizer, ::BD.ObjectiveDualBound, db) set_initial_dual_bound!(model.inner, db) return end -function MOI.set(model::Coluna.Optimizer, ::BD.ObjectivePrimalBound, pb) +function MOI.set(model::Optimizer, ::BD.ObjectivePrimalBound, pb) set_initial_primal_bound!(model.inner, pb) return end -function _customdata!(model::Coluna.Optimizer, type::DataType) +function _customdata!(model::Optimizer, type::DataType) haskey(model.env.custom_families_id, type) && return model.env.custom_families_id[type] = length(model.env.custom_families_id) return end function MOI.set( - model::Coluna.Optimizer, ::BD.CustomVars, customvars::Vector{DataType} + model::Optimizer, ::BD.CustomVars, customvars::Vector{DataType} ) for customvar in customvars _customdata!(model, customvar) @@ -690,7 +762,7 @@ function MOI.set( end function MOI.set( - model::Coluna.Optimizer, ::BD.CustomConstrs, customconstrs::Vector{DataType} + model::Optimizer, ::BD.CustomConstrs, customconstrs::Vector{DataType} ) for customconstr in customconstrs _customdata!(model, customconstr) @@ -698,15 +770,16 @@ function MOI.set( return end -function MOI.empty!(model::Coluna.Optimizer) +function MOI.empty!(model::Optimizer) model.inner = Problem(model.env) model.annotations = Annotations() model.vars = CleverDicts.CleverDict{MOI.VariableIndex, Variable}() model.env.varids = CleverDicts.CleverDict{MOI.VariableIndex, VarId}() model.moi_varids = Dict{VarId, MOI.VariableIndex}() model.constrs = Dict{MOI.ConstraintIndex, Constraint}() - model.constrs_on_single_var_to_vars = Dict{MOI.ConstraintIndex, VarId}() - model.constrs_on_single_var_to_names = Dict{MOI.ConstraintIndex, String}() + model.constrs_on_single_var = Dict{MOI.ConstraintIndex, BoundConstraints}() + #model.constrs_on_single_var_to_vars = Dict{MOI.ConstraintIndex, VarId}() + #model.constrs_on_single_var_to_names = Dict{MOI.ConstraintIndex, String}() if model.default_optimizer_builder !== nothing set_default_optimizer_builder!(model.inner, model.default_optimizer_builder) end @@ -716,12 +789,12 @@ function MOI.empty!(model::Coluna.Optimizer) end mutable struct ColumnInfo <: BD.AbstractColumnInfo - optimizer::Coluna.Optimizer + optimizer::Optimizer column_var_id::VarId column_val::Float64 end -function BD.getsolutions(model::Coluna.Optimizer, k) +function BD.getsolutions(model::Optimizer, k) ip_primal_sol = get_best_ip_primal_sol(model.disagg_result) sp_columns_info = Vector{ColumnInfo}() for (varid, val) in ip_primal_sol @@ -743,7 +816,7 @@ function BD.value(info::ColumnInfo, index::MOI.VariableIndex) return getprimalsolmatrix(spform)[varid, info.column_var_id] end -function MOI.get(model::Coluna.Optimizer, ::MOI.NumberOfVariables) +function MOI.get(model::Optimizer, ::MOI.NumberOfVariables) orig_form = get_original_formulation(model.inner) return length(getvars(orig_form)) end @@ -772,7 +845,7 @@ end function MOI.is_valid( optimizer::Optimizer, index::MOI.ConstraintIndex{F,S} ) where {F<:MOI.SingleVariable,S} - return haskey(optimizer.constrs_on_single_var_to_names, index) + return haskey(optimizer.constrs_on_single_var, index) end function MOI.is_valid( @@ -802,7 +875,7 @@ function MOI.get(optimizer::Optimizer, ::MOI.RelativeGap) end function MOI.get(optimizer::Optimizer, attr::MOI.VariablePrimal, ref::MOI.VariableIndex) - id = getid(optimizer.vars[ref]) # This gets a coluna Id{Variable} + id = getid(optimizer.vars[ref]) # This gets a coluna VarId primalsols = get_ip_primal_sols(optimizer.result) if 1 <= attr.N <= length(primalsols) return get(primalsols[attr.N], id, 0.0) @@ -846,16 +919,18 @@ end function MOI.get( optimizer::Optimizer, ::MOI.ConstraintPrimal, index::MOI.ConstraintIndex{F,S} ) where {F<:MOI.SingleVariable,S} - varid = get(optimizer.constrs_on_single_var_to_vars, index, nothing) - if varid === nothing + MOI.throw_if_not_valid(optimizer, index) + bounds = get(optimizer.constrs_on_single_var, index, nothing) + if bounds === nothing @warn "Could not find constraint with id $(index)." return NaN end best_primal_sol = get_best_ip_primal_sol(optimizer.result) - return get(best_primal_sol, varid, 0.0) + return get(best_primal_sol, bounds.varid, 0.0) end function MOI.get(optimizer::Optimizer, ::MOI.ConstraintPrimal, index::MOI.ConstraintIndex) + MOI.throw_if_not_valid(optimizer, index) constrid = get(optimizer.constrs, index, nothing) if constrid === nothing @warn "Could not find constraint with id $(index)." @@ -872,6 +947,7 @@ function MOI.get( optimizer::Optimizer, attr::MOI.ConstraintDual, index::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}} ) + MOI.throw_if_not_valid(optimizer, index) dualsols = get_lp_dual_sols(optimizer.result) if 1 <= attr.N <= length(dualsols) return get(dualsols[attr.N], getid(optimizer.constrs[index]), 0.0) @@ -879,6 +955,39 @@ function MOI.get( return error("Invalid result index.") end +function _singlevarconstrdualval(bc, dualsol, ::Type{<:MOI.GreaterThan}) + value, activebound = get(get_var_redcosts(dualsol), bc.varid, (0.0, MathProg.LOWER)) + if value != 0.0 && activebound != MathProg.LOWER + return 0.0 + end + return value +end + +function _singlevarconstrdualval(bc, dualsol, ::Type{<:MOI.LessThan}) + value, activebound = get(get_var_redcosts(dualsol), bc.varid, (0.0, MathProg.UPPER)) + if value != 0.0 && activebound != MathProg.UPPER + return 0.0 + end + return value +end + +function _singlevarconstrdualval(bc, dualsol, ::Type{<:MOI.EqualTo}) + value, _ = get(get_var_redcosts(dualsol), bc.varid, (0.0, MathProg.LOWER)) + return value +end + +function MOI.get( + optimizer::Optimizer, attr::MOI.ConstraintDual, index::MOI.ConstraintIndex{F,S} +) where {F<:MOI.SingleVariable,S} + MOI.throw_if_not_valid(optimizer, index) + dualsols = get_lp_dual_sols(optimizer.result) + if 1 <= attr.N <= length(dualsols) + single_var_constrs = optimizer.constrs_on_single_var[index] + return _singlevarconstrdualval(single_var_constrs, dualsols[attr.N], S) + end + return error("Invalid result index.") +end + # Useful method to retrieve dual values of generated cuts because they don't # have MOI.ConstraintIndex function MOI.get( @@ -889,14 +998,4 @@ function MOI.get( return get(dualsols[attr.N], constrid, 0.0) end return error("Invalid result index.") -end - -# function MOI.get( -# optimizer::Optimizer, attr::MOI.ConstraintDual, index::MOI.ConstraintIndex{F,S} -# ) where {F<:MOI.SingleVariable,S} -# return 0.0 -# end - -# function MOI.get(optimizer::Optimizer, ::MOI.SolveTime) -# return 0.0 -# end \ No newline at end of file +end \ No newline at end of file diff --git a/src/MathProg/MOIinterface.jl b/src/MathProg/MOIinterface.jl index d7313ce46..be94ac63e 100644 --- a/src/MathProg/MOIinterface.jl +++ b/src/MathProg/MOIinterface.jl @@ -39,6 +39,7 @@ function update_bounds_in_optimizer!(form::Formulation, optimizer::MoiOptimizer, MOI.Interval(getcurlb(form, var), getcurub(form, var)) )) end + return end function update_cost_in_optimizer!(form::Formulation, optimizer::MoiOptimizer, var::Variable) @@ -59,9 +60,9 @@ function update_obj_const_in_optimizer!(form::Formulation, optimizer::MoiOptimiz return end -function update_constr_member_in_optimizer!(optimizer::MoiOptimizer, - c::Constraint, v::Variable, - coeff::Float64) +function update_constr_member_in_optimizer!( + optimizer::MoiOptimizer, c::Constraint, v::Variable, coeff::Float64 +) moi_c_index = getindex(getmoirecord(c)) moi_v_index = getindex(getmoirecord(v)) MOI.modify( @@ -153,12 +154,9 @@ function add_to_optimizer!( return end -function remove_from_optimizer!(form::Formulation, optimizer::MoiOptimizer, ids::Set{Id{T}}) where { - T <: AbstractVarConstr} +function remove_from_optimizer!(form::Formulation, optimizer::MoiOptimizer, ids::Set{I}) where {I<:Id} 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) + remove_from_optimizer!(form, optimizer, getelem(form, id)) end return end @@ -199,7 +197,7 @@ function _getreducedcost(form::Formulation, optimizer, var::Variable) Cannot retrieve reduced cost of variable $varname from formulation solved with optimizer of type $opt. Method returns nothing. """ - return nothing + return end function getreducedcost(form::Formulation, optimizer::MoiOptimizer, var::Variable) @@ -210,7 +208,7 @@ function getreducedcost(form::Formulation, optimizer::MoiOptimizer, var::Variabl No dual solution stored in the optimizer of formulation. Cannot retrieve reduced costs. Method returns nothing. """ - return nothing + return end if !iscuractive(form, var) || !isexplicit(form, var) varname = getname(form, var) @@ -218,7 +216,7 @@ function getreducedcost(form::Formulation, optimizer::MoiOptimizer, var::Variabl Cannot retrieve reduced cost of variable $varname because the variable must be active and explicit. Method returns nothing. """ - return nothing + return end bounds_interval_idx = getbounds(getmoirecord(var)) dualval = MOI.get(inner, MOI.ConstraintDual(1), bounds_interval_idx) @@ -234,19 +232,20 @@ function get_primal_solutions(form::F, optimizer::MoiOptimizer) where {F <: Form if MOI.get(inner, MOI.PrimalStatus(res_idx)) != MOI.FEASIBLE_POINT continue end + solcost = getobjconst(form) - solvars = Vector{VarId}() - solvals = Vector{Float64}() + solvars = VarId[] + solvals = Float64[] + + # Get primal values of variables for (id, var) in getvars(form) iscuractive(form, id) && isexplicit(form, id) || continue moirec = getmoirecord(var) moi_index = getindex(moirec) - kind = _getcolunakind(moirec) val = MOI.get(inner, MOI.VariablePrimal(res_idx), moi_index) solcost += val * getcurcost(form, id) val = round(val, digits = Coluna.TOL_DIGITS) if abs(val) > Coluna.TOL - @logmsg LogLevel(-4) string("Var ", var.name , " = ", val) push!(solvars, id) push!(solvals, val) end @@ -256,21 +255,28 @@ function get_primal_solutions(form::F, optimizer::MoiOptimizer) where {F <: Form return solutions end +# Retrieve dual solutions stored in the optimizer of a formulation +# It works only if the optimizer is wrapped with MathOptInterface. function get_dual_solutions(form::F, optimizer::MoiOptimizer) where {F <: Formulation} inner = getinner(optimizer) nb_dual_sols = MOI.get(inner, MOI.ResultCount()) solutions = DualSolution{F}[] + for res_idx in 1:nb_dual_sols + # We retrieve only feasible dual solutions if MOI.get(inner, MOI.DualStatus(res_idx)) != MOI.FEASIBLE_POINT continue end + + # Cost of the dual solution solcost = getobjconst(form) - solconstrs = Vector{ConstrId}() - solvals = Vector{Float64}() + # Get dual value of constraints + solconstrs = ConstrId[] + solvals = Float64[] for (id, constr) in getconstrs(form) - iscuractive(form, id) && isexplicit(form, id) || continue moi_index = getindex(getmoirecord(constr)) + MOI.is_valid(inner, moi_index) || continue val = MOI.get(inner, MOI.ConstraintDual(res_idx), moi_index) solcost += val * getcurrhs(form, id) val = round(val, digits = Coluna.TOL_DIGITS) @@ -279,22 +285,47 @@ function get_dual_solutions(form::F, optimizer::MoiOptimizer) where {F <: Formul push!(solvals, val) end end - # Get reduced cost of variables - for (id, var) in getvars(form) - iscuractive(form, id) && isexplicit(form, id) || continue + + # Get dual value & active bound of variables + var_red_costs = Dict{VarId, Tuple{Float64,ActiveBound}}() + varids = VarId[] + varvals = Float64[] + activebounds = ActiveBound[] + for (varid, var) in getvars(form) moi_bounds_index = getbounds(getmoirecord(var)) + MOI.is_valid(inner, moi_bounds_index) || continue basis_status = MOI.get(inner, MOI.ConstraintBasisStatus(res_idx), moi_bounds_index) val = MOI.get(inner, MOI.ConstraintDual(res_idx), moi_bounds_index) + + # Variables with non-zero dual values have at least one active bound. + # Otherwise, we print a warning message. if basis_status == MOI.NONBASIC_AT_LOWER - solcost += val * getcurlb(form, id) + solcost += val * getcurlb(form, varid) + if abs(val) > Coluna.TOL + push!(varids, varid) + push!(varvals, val) + push!(activebounds, LOWER) + end elseif basis_status == MOI.NONBASIC_AT_UPPER - solcost += val * getcurub(form, id) + solcost += val * getcurub(form, varid) + if abs(val) > Coluna.TOL + push!(varids, varid) + push!(varvals, val) + push!(activebounds, UPPER) + end + elseif abs(val) > Coluna.TOL + @warn """ + Basis status of a variable that has a non-zero dual value is not treated. + Basis status is $basis_status & dual value is $val. + """ end - # TODO : store reduced cost of the variable in the dual constraint ? end - sense = getobjsense(form) == MaxSense ? -1.0 : 1.0 - push!(solutions, DualSolution(form, solconstrs, solvals, sense * solcost, FEASIBLE_SOL)) + sense = getobjsense(form) == MinSense ? 1.0 : -1.0 + push!(solutions, DualSolution( + form, solconstrs, solvals, varids, varvals, activebounds, sense*solcost, + FEASIBLE_SOL + )) end return solutions end diff --git a/src/MathProg/MathProg.jl b/src/MathProg/MathProg.jl index c96104e32..801cfb968 100644 --- a/src/MathProg/MathProg.jl +++ b/src/MathProg/MathProg.jl @@ -28,10 +28,10 @@ include("vcids.jl") include("variable.jl") include("constraint.jl") include("duties.jl") -include("manager.jl") include("bounds.jl") include("solutions.jl") include("buffer.jl") +include("manager.jl") include("formulation.jl") include("varconstr.jl") include("optimizerwrappers.jl") @@ -53,8 +53,6 @@ export no_optimizer_builder, set_original_formulation!, getid, getuid, enforce_integrality!, relax_integrality!, getobjsense, getoptimizer, getoptimizers, - setdualbound!, - computereducedcost, update!, getduty, computereducedrhs, @@ -83,7 +81,7 @@ export AbstractFormulation, Formulation, create_formulation!, getreformulation, 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_optimizer!, - push_optimizer!, getobjconst, setobjconst!, addcustomvars!, addcustomconstrs! + push_optimizer!, getobjconst, setobjconst!, addcustomvars!, addcustomconstrs!, clonesinglevarconstr!, getsinglevarconstrs, getsinglevarconstr # Duties of formulations export Original, DwMaster, BendersMaster, DwSp, BendersSp @@ -92,15 +90,16 @@ export Original, DwMaster, BendersMaster, DwSp, BendersSp export isanArtificialDuty, isaStaticDuty, isaDynamicDuty, isanOriginalRepresentatives # Types and methods related to variables and constraints -export Variable, Constraint, VarId, ConstrId, VarMembership, ConstrMembership, +export Variable, Constraint, VarId, ConstrId, SingleVarConstrId, VarMembership, ConstrMembership, SingleVarConstraint, getperencost, setperencost!, getcurcost, setcurcost!, getperenlb, getcurlb, setcurlb!, getperenub, getcurub, setcurub!, getperenrhs, setperenrhs!, getcurrhs, setcurrhs!, getperensense, setperensense!, getcursense, setcursense!, getperenkind, getcurkind, setcurkind!, getperenincval, getcurincval, setcurincval!, isperenactive, iscuractive, activate!, deactivate!, - isexplicit, getname, getbranchingpriority, reset!, getreducedcost + isexplicit, getname, getbranchingpriority, reset!, getreducedcost, setperenkind!, setsinglevarconstr! # Types & methods related to solutions & bounds -export PrimalBound, DualBound, PrimalSolution, DualSolution, ObjValues +export PrimalBound, DualBound, AbstractSolution, PrimalSolution, DualSolution, ActiveBound, ObjValues, + get_var_redcosts # Methods related to projections export projection_is_possible, proj_cols_on_rep diff --git a/src/MathProg/bounds.jl b/src/MathProg/bounds.jl index 453ad8d46..64fdba700 100644 --- a/src/MathProg/bounds.jl +++ b/src/MathProg/bounds.jl @@ -103,7 +103,7 @@ _lp_gap(ov::ObjValues) = gap(ov.lp_primal_bound, ov.lp_dual_bound) function _ip_gap_closed( ov::ObjValues; atol = Coluna.DEF_OPTIMALITY_ATOL, rtol = Coluna.DEF_OPTIMALITY_RTOL ) - return (_ip_gap(ov) <= 0) || _gap_closed( + return _ip_gap(ov) <= 0 || _gap_closed( ov.ip_primal_bound.value, ov.ip_dual_bound.value, atol = atol, rtol = rtol ) end @@ -111,7 +111,7 @@ end function _lp_gap_closed( ov::ObjValues; atol = Coluna.DEF_OPTIMALITY_ATOL, rtol = Coluna.DEF_OPTIMALITY_RTOL ) - return (_lp_gap(ov) <= 0) || _gap_closed( + return _lp_gap(ov) <= 0 || _gap_closed( ov.lp_primal_bound.value, ov.lp_dual_bound.value, atol = atol, rtol = rtol ) end @@ -120,7 +120,7 @@ function _gap_closed( x::Number, y::Number; atol::Real = 0, rtol::Real = atol > 0 ? 0 : √eps, norm::Function = abs ) - return (x == y) || (isfinite(x) && isfinite(y) && norm(x - y) <= max(atol, rtol*min(norm(x), norm(y)))) + return x == y || (isfinite(x) && isfinite(y) && norm(x - y) <= max(atol, rtol*min(norm(x), norm(y)))) end ## Bound updates diff --git a/src/MathProg/buffer.jl b/src/MathProg/buffer.jl index 18ada93f5..3b949cca2 100644 --- a/src/MathProg/buffer.jl +++ b/src/MathProg/buffer.jl @@ -1,15 +1,15 @@ """ -A `VarConstrBuffer{T}` stores the ids of the entities of type `T` that will be -added and removed from a formulation. +A `VarConstrBuffer{I}` stores the ids of type `I` of the variables, constraints or +single variable constraints that will be added and removed from a formulation. """ -mutable struct VarConstrBuffer{T<:AbstractVarConstr} - added::Set{Id{T}} - removed::Set{Id{T}} +mutable struct VarConstrBuffer{I<:Id} + added::Set{I} + removed::Set{I} end -VarConstrBuffer{T}() where {T<:AbstractVarConstr} = VarConstrBuffer{T}(Set{T}(), Set{T}()) +VarConstrBuffer{I}() where {I<:Id} = VarConstrBuffer{I}(Set{I}(), Set{I}()) -function add!(buffer::VarConstrBuffer{VC}, id::Id{VC}) where {VC<:AbstractVarConstr} +function add!(buffer::VarConstrBuffer{I}, id::I) where {I<:Id} if id ∉ buffer.removed push!(buffer.added, id) else @@ -18,7 +18,7 @@ function add!(buffer::VarConstrBuffer{VC}, id::Id{VC}) where {VC<:AbstractVarCon return end -function remove!(buffer::VarConstrBuffer{VC}, id::Id{VC}) where {VC<:AbstractVarConstr} +function remove!(buffer::VarConstrBuffer{I}, id::I) where {I<:Id} if id ∉ buffer.added push!(buffer.removed, id) else @@ -41,15 +41,16 @@ mutable struct FormulationBuffer changed_bound::Set{VarId} # bound of a variable changed_var_kind::Set{VarId} # kind of a variable changed_rhs::Set{ConstrId} # rhs and sense of a constraint - var_buffer::VarConstrBuffer{Variable} # variable added or removed - constr_buffer::VarConstrBuffer{Constraint} # constraint added or removed + var_buffer::VarConstrBuffer{VarId} # variable added or removed + constr_buffer::VarConstrBuffer{ConstrId} # constraint added or removed + singlevarconstr_buffer::VarConstrBuffer{SingleVarConstrId} # single var constraint added or removed reset_coeffs::Dict{Pair{ConstrId,VarId},Float64} # coefficient of the matrix changed end FormulationBuffer() = FormulationBuffer( false, false, Set{VarId}(), Set{VarId}(), Set{VarId}(), Set{ConstrId}(), - VarConstrBuffer{Variable}(), VarConstrBuffer{Constraint}(), - Dict{Pair{ConstrId,VarId},Float64}() + VarConstrBuffer{VarId}(), VarConstrBuffer{ConstrId}(), + VarConstrBuffer{SingleVarConstrId}(), Dict{Pair{ConstrId,VarId},Float64}() ) add!(b::FormulationBuffer, varid::VarId) = add!(b.var_buffer, varid) @@ -66,7 +67,7 @@ function remove!(buffer::FormulationBuffer, varid::VarId) end # Since there is no efficient way to remove changes done to the coefficient matrix, -# we propagate them if the constraint is active and explicit +# we propagate them if and only if the constraint is active and explicit function remove!(buffer::FormulationBuffer, constrid::ConstrId) remove!(buffer.constr_buffer, constrid) delete!(buffer.changed_rhs, constrid) diff --git a/src/MathProg/clone.jl b/src/MathProg/clone.jl index ef3b8030e..a21ce75a8 100644 --- a/src/MathProg/clone.jl +++ b/src/MathProg/clone.jl @@ -21,7 +21,7 @@ function clonevar!( cost = cost, lb = lb, ub = ub, kind = kind, inc_val = inc_val, is_active = is_active, is_explicit = is_explicit, branching_priority = branching_priority, members = members, - id = Id{Variable}(duty, getid(var), getuid(assignedform)) + id = VarId(duty, getid(var), getuid(assignedform)) ) end @@ -46,7 +46,29 @@ function cloneconstr!( rhs = rhs, kind = kind, sense = sense, inc_val = inc_val, is_active = is_active, is_explicit = is_explicit, members = members, loc_art_var_abs_cost = loc_art_var_abs_cost, - id = Id{Constraint}(duty, getid(constr), getuid(assignedform)) + id = ConstrId(duty, getid(constr), getuid(assignedform)) + ) +end + +function clonesinglevarconstr!( + originform::Formulation, + destform::Formulation, + assignedform::Formulation, + constr::SingleVarConstraint, + duty::Duty{Constraint}; + name::String = constr.name, #getname(originform, constr), + rhs::Float64 = constr.perendata.rhs, #getperenrhs(originform, constr), + kind::ConstrKind = constr.perendata.kind, #getperenkind(originform, constr), + sense::ConstrSense = constr.perendata.sense, #getperensense(originform, constr), + inc_val::Float64 = constr.perendata.inc_val, #getperenincval(originform, constr), + is_active::Bool = true, #isperenactive(originform, constr), + is_explicit::Bool = true #isexplicit(originform, constr) +) + return setsinglevarconstr!( + destform, name, constr.varid, duty; + rhs = rhs, kind = kind, sense = sense, inc_val = inc_val, + is_active = is_active, + id = SingleVarConstrId(duty, getid(constr), getuid(assignedform)) ) end diff --git a/src/MathProg/constraint.jl b/src/MathProg/constraint.jl index a21b78e61..09ac4ab93 100644 --- a/src/MathProg/constraint.jl +++ b/src/MathProg/constraint.jl @@ -1,7 +1,6 @@ """ - ConstrData - -Information that defines a state of a constraint. These are the fields of a constraint that might change during the solution procedure. +Information that defines a state of a constraint. +These data might change during the optimisation procedure. """ mutable struct ConstrData <: AbstractVcData rhs::Float64 @@ -27,11 +26,7 @@ ConstrData(cd::ConstrData) = ConstrData( cd.rhs, cd.kind, cd.sense, cd.inc_val, cd.is_active, cd.is_explicit ) -""" - MoiConstrRecord - -Structure to hold the pointers to the MOI representation of a Coluna Constraint. -""" +"Structure to hold the pointers to the MOI representation of a Coluna Constraint." mutable struct MoiConstrRecord index::MoiConstrIndex end @@ -42,12 +37,19 @@ getindex(record::MoiConstrRecord) = record.index setindex!(record::MoiConstrRecord, index::MoiConstrIndex) = record.index = index """ - Constraint +There are 2 types of constraints in Coluna (i.e. Constraint & SingleVarConstraint). +Both of them inherits from AbstractConstraint because their setters and getters +are quite similar. +""" +abstract type AbstractConstraint <: AbstractVarConstr end +""" Representation of a constraint in Coluna. +Coefficients of variables involved in the constraints are stored in the coefficient matrix. +If the constraint involves only one variable, you should use a `SingleVarConstraint`. """ -mutable struct Constraint <: AbstractVarConstr - id::Id{Constraint} +mutable struct Constraint <: AbstractConstraint + id::Id{Constraint,:usual} name::String perendata::ConstrData curdata::ConstrData @@ -56,8 +58,9 @@ mutable struct Constraint <: AbstractVarConstr custom_data::Union{Nothing, BD.AbstractCustomData} end -const ConstrId = Id{Constraint} +const ConstrId = Id{Constraint,:usual} +# Internal use only, see `MathProg.setconstr!` to create a constraint. function Constraint( id::ConstrId, name::String; constr_data = ConstrData(), moi_index::MoiConstrIndex = MoiConstrIndex(), @@ -69,6 +72,33 @@ function Constraint( ) end +""" +Representation of a single variable constraint in Coluna : lb <= 1*var <= ub. +For performance reasons, Coluna does not store these constraints in the coefficient matrix +and they are never pushed in the subsolver of a formulation. +Coluna takes into account those constraints only during the bound propagation operation +which updates the current upper and lower bounds of the variable. +""" +mutable struct SingleVarConstraint <: AbstractConstraint + id::Id{Constraint,:single} + name::String + varid::VarId + perendata::ConstrData + curdata::ConstrData +end + +const SingleVarConstrId = Id{Constraint,:single} + +# Internal use only, see `MathProg.setsinglevarconstr!` to create a single var constraint. +function SingleVarConstraint( + id::SingleVarConstrId, varid::VarId, name::String; constr_data = ConstrData() +) + return SingleVarConstraint(id, name, varid, constr_data, ConstrData(constr_data)) +end + +""" +Constraints generator (cut callback). +""" mutable struct RobustConstraintsGenerator nb_generated::Int kind::ConstrKind diff --git a/src/MathProg/duties.jl b/src/MathProg/duties.jl index 2b87c361c..be2d217d1 100644 --- a/src/MathProg/duties.jl +++ b/src/MathProg/duties.jl @@ -1,6 +1,4 @@ -# # Duties for a Formulation -# abstract type AbstractFormDuty end abstract type AbstractMasterDuty <: AbstractFormDuty end abstract type AbstractSpDuty <: AbstractFormDuty end @@ -29,9 +27,7 @@ end BendersSp() = BendersSp(Dict{VarId, VarId}()) -# # Duties tree for a Variable -# @exported_nestedenum begin Duty{Variable} AbstractOriginalVar <= Duty{Variable} @@ -65,9 +61,7 @@ BendersSp() = BendersSp(Dict{VarId, VarId}()) BendSpPrimalSol <= AbstractBendSpVar end -# # Duties tree for a Constraint -# @exported_nestedenum begin Duty{Constraint} AbstractOriginalConstr <= Duty{Constraint} @@ -101,9 +95,7 @@ end BendSpDualSol <= AbstractBendSpConstr end -# # Methods to get extra information about duties -# function isaStaticDuty(duty::NestedEnum) return duty <= OriginalVar || #duty <= OriginalExpression || diff --git a/src/MathProg/formulation.jl b/src/MathProg/formulation.jl index ceae372f6..b9e347d5e 100644 --- a/src/MathProg/formulation.jl +++ b/src/MathProg/formulation.jl @@ -21,7 +21,7 @@ A `Formulation` stores a mixed-integer linear program. obj_sense::Type{<:Coluna.AbstractSense} = MinSense ) -Create a new formulation in the Coluna's environment `env`. +Creates a new formulation in the Coluna's environment `env`. Arguments are `duty` that contains specific information related to the duty of the formulation, `parent_formulation` that is the parent formulation (master for a subproblem, reformulation for a master, `nothing` by default), and `obj_sense` the sense of the objective @@ -52,40 +52,59 @@ ColunaBase.getstorage(form::Formulation) = form.storage """ haskey(formulation, id) -> Bool -Return `true` if `formulation` has a variable or a constraint with given `id`. +Returns `true` if `formulation` has a variable or a constraint with given `id`. """ haskey(form::Formulation, id::VarId) = haskey(form.manager.vars, id) -haskey(form::Formulation, id::ConstrId) = haskey(form.manager.constrs, id) +haskey(form::Formulation, id::ConstrId) = haskey(form.manager.constrs, id) || haskey(form.manager.single_var_constrs, id) """ getvar(formulation, varid) -> Variable -Return the variable with given `varid` that belongs to `formulation`. +Returns the variable with given `varid` that belongs to `formulation`. """ getvar(form::Formulation, id::VarId) = get(form.manager.vars, id, nothing) """ getconstr(formulation, constrid) -> Constraint + getconstr(formulation, singlevarconstrid) -> Constraint -Return the constraint with given `constrid` that belongs to `formulation`. +Returns the constraint with given `constrid` that belongs to `formulation`. """ getconstr(form::Formulation, id::ConstrId) = get(form.manager.constrs, id, nothing) +getconstr(form::Formulation, id::SingleVarConstrId) = get(form.manager.single_var_constrs, id, nothing) """ getvars(formulation) -> Dict{VarId, Variable} -Return all variables in `formulation`. +Returns all variables in `formulation`. """ getvars(form::Formulation) = form.manager.vars """ getconstrs(formulation) -> Dict{ConstrId, Constraint} -Return all constraints in `formulation`. +Returns all constraints in `formulation`. """ getconstrs(form::Formulation) = form.manager.constrs +""" + getsinglevarconstrs(formulation) -> Dict{ConstrId, SingleVarConstraint} + +Returns all single variable constraints in `formulation`. + + getsinglevarconstrs(formulation, varid) -> Dict{ConstrId, SingleVarConstraint} or nothing + +Return all single variable constraints of a formulation that applies to a given variable. +""" +getsinglevarconstrs(form::Formulation) = form.manager.single_var_constrs +getsinglevarconstrs(form::Formulation, varid::VarId) = get( + form.manager.single_var_constrs_per_var, varid, Dict{ConstrId, SingleVarConstraint}() +) + +"Returns objective constant of the formulation." getobjconst(form::Formulation) = form.manager.objective_constant + +"Sets objective constant of the formulation." function setobjconst!(form::Formulation, val::Float64) form.manager.objective_constant = val form.buffer.changed_obj_const = true @@ -94,34 +113,51 @@ end "Returns the representation of the coefficient matrix stored in the formulation manager." getcoefmatrix(form::Formulation) = form.manager.coefficients + getprimalsolmatrix(form::Formulation) = form.manager.primal_sols getprimalsolcosts(form::Formulation) = form.manager.primal_sol_costs getdualsolmatrix(form::Formulation) = form.manager.dual_sols getdualsolrhss(form::Formulation) = form.manager.dual_sol_rhss -"Returns the `uid` of `Formulation` `form`." +"Returns the `uid` of a formulation." getuid(form::Formulation) = form.uid -"Returns the objective function sense of `Formulation` `form`." +"Returns the objective function sense of a formulation." getobjsense(form::Formulation) = form.obj_sense -"Returns the `AbstractOptimizer` of `Formulation` `form`." -function getoptimizer(form::Formulation, id::Int) - if id <= 0 && id > length(form.optimizers) +"Returns the optimizer of a formulation at a given position." +function getoptimizer(form::Formulation, pos::Int) + if pos <= 0 && pos > length(form.optimizers) return NoOptimizer() end - return form.optimizers[id] + return form.optimizers[pos] end + +"Returns all the optimizers of a formulation." getoptimizers(form::Formulation) = form.optimizers +""" + getelem(form, varid) -> Variable + getelem(form, constrid) -> Constraint + getelem(form, singlevarconstrid) -> SingleVarConstraint + +Return the element of formulation `form` that has a given id. +""" getelem(form::Formulation, id::VarId) = getvar(form, id) getelem(form::Formulation, id::ConstrId) = getconstr(form, id) +getelem(form::Formulation, id::SingleVarConstrId) = getconstr(form, id) + +generatevarid(duty::Duty{Variable}, form::Formulation) = + VarId(duty, form.var_counter += 1, getuid(form), -1) + +generatevarid(duty::Duty{Variable}, form::Formulation, custom_family_id::Int) = + VarId(duty, form.var_counter += 1, getuid(form), custom_family_id) + +generateconstrid(duty::Duty{Constraint}, form::Formulation) = + ConstrId(duty, form.constr_counter += 1, getuid(form), -1) -generatevarid(duty::Duty{Variable}, form::Formulation) = VarId(duty, form.var_counter += 1, getuid(form), -1) -generatevarid( - duty::Duty{Variable}, form::Formulation, custom_family_id::Int -) = VarId(duty, form.var_counter += 1, getuid(form), custom_family_id) -generateconstrid(duty::Duty{Constraint}, form::Formulation) = ConstrId(duty, form.constr_counter += 1, getuid(form), -1) +generatesinglevarconstrid(duty::Duty{Constraint}, form::Formulation) = + SingleVarConstrId(duty, form.constr_counter += 1, getuid(form), -1) getmaster(form::Formulation{<:AbstractSpDuty}) = form.parent_formulation getreformulation(form::Formulation{<:AbstractMasterDuty}) = form.parent_formulation @@ -132,7 +168,7 @@ getstoragedict(form::Formulation) = form.storage.units _reset_buffer!(form::Formulation) = form.buffer = FormulationBuffer() """ - set_matrix_coeff!(form::Formulation, v_id::Id{Variable}, c_id::Id{Constraint}, new_coeff::Float64) + set_matrix_coeff!(form::Formulation, v_id::VarId, c_id::ConstrId, new_coeff::Float64) Buffers the matrix modification in `form.buffer` to be sent to the optimizers right before next call to optimize!. """ @@ -144,7 +180,7 @@ set_matrix_coeff!( setvar!( formulation, name, duty; cost = 0.0, - lb = 0.0, + lb = -Inf, ub = Inf, kind = Continuous, is_active = true, @@ -168,7 +204,7 @@ function setvar!( name::String, duty::Duty{Variable}; cost::Float64 = 0.0, - lb::Float64 = 0.0, + lb::Float64 = -Inf, ub::Float64 = Inf, kind::VarKind = Continuous, inc_val::Float64 = 0.0, @@ -237,7 +273,7 @@ function setprimalsol!(form::Formulation, new_primal_sol::PrimalSolution)::Tuple # look for an identical column for (cur_sol_id, cur_cost) in primal_sol_costs cur_primal_sol = primal_sols[:, cur_sol_id] - if isapprox(new_cost, cur_cost) && getsol(new_primal_sol) == cur_primal_sol + if isapprox(new_cost, cur_cost) && new_primal_sol == cur_primal_sol return (false, cur_sol_id) end end @@ -259,6 +295,14 @@ function _adddualsol!(form::Formulation, dualsol::DualSolution, dualsol_id::Cons form.manager.dual_sols[constrid, dualsol_id] = constrval end end + for (varid, varval) in get_var_redcosts(dualsol) + redcost, activebound = varval + bound = activebound == LOWER ? getcurlb(form, varid) : getcurub(form, varid) + rhs += bound * redcost + if getduty(varid) <= AbstractBendSpMasterConstr + form.manager.dual_sols_varbounds[varid, dualsol_id] = varval + end + end form.manager.dual_sol_rhss[dualsol_id] = rhs return dualsol_id end @@ -266,6 +310,7 @@ end function setdualsol!(form::Formulation, new_dual_sol::DualSolution)::Tuple{Bool,ConstrId} ### check if dualsol exists take place here along the coeff update dual_sols = getdualsolmatrix(form) + dual_sols_varbounds = form.manager.dual_sols_varbounds dual_sol_rhss = getdualsolrhss(form) for (cur_sol_id, cur_rhs) in dual_sol_rhss @@ -276,16 +321,17 @@ function setdualsol!(form::Formulation, new_dual_sol::DualSolution)::Tuple{Bool, # TODO : implement broadcasting for PMA in DynamicSparseArrays is_identical = true - cur_dual_sol = dual_sols[cur_sol_id, :] + cur_dual_sol = @view dual_sols[:,cur_sol_id] for (constr_id, constr_val) in cur_dual_sol - if factor * getsol(new_dual_sol)[constr_id] != constr_val + if factor * new_dual_sol.solution.sol[constr_id] != constr_val is_identical = false break end end - for (constr_id, constr_val) in getsol(new_dual_sol) - if factor * constr_val != cur_dual_sol[constr_id] + cur_dual_sol_varbounds = @view dual_sols_varbounds[:,cur_sol_id] + for (var_id, var_val) in cur_dual_sol_varbounds + if factor * get_var_redcosts(new_dual_sol)[var_id][1] != var_val[1] || get_var_redcosts(new_dual_sol)[var_id][2] != var_val[2] is_identical = false break end @@ -354,7 +400,7 @@ function setcut_from_sp_dualsol!( objc = getobjsense(masterform) === MinSense ? 1.0 : -1.0 rhs = objc * getdualsolrhss(spform)[dual_sol_id] - benders_cut_id = Id{Constraint}(duty, dual_sol_id) + benders_cut_id = ConstrId(duty, dual_sol_id) benders_cut_data = ConstrData( rhs, Essential, sense, inc_val, is_active, is_explicit ) @@ -393,6 +439,14 @@ function setcut_from_sp_dualsol!( end end + # sp_dual_sol = spform.manager.dual_sols_varbounds[:,dual_sol_id] + # for (var_id, var_val) in sp_dual_sol + # var_val, active_bound = var_val + # if getduty(var_id) <= AbstractBendSpVar + # orig_var_id + # end + # end + _addconstr!(masterform.manager, benders_cut) if isexplicit(masterform, benders_cut) @@ -444,7 +498,8 @@ function setconstr!( end if custom_data !== nothing id = ConstrId( - duty, id, custom_family_id = form.manager.custom_families_id[typeof(custom_data)] + duty, id, + custom_family_id = form.manager.custom_families_id[typeof(custom_data)] ) end c_data = ConstrData(rhs, kind, sense, inc_val, is_active, is_explicit) @@ -461,6 +516,29 @@ function setconstr!( return constr end +""" +TODO + +This constraint is never explicit because it's used to compute bounds stored in the variable +""" +function setsinglevarconstr!( + form::Formulation, + name::String, + varid::VarId, + duty::Duty{Constraint}; + rhs::Float64 = 0.0, + kind::ConstrKind = Essential, + sense::ConstrSense = Greater, + inc_val::Float64 = 0.0, + is_active::Bool = true, + id = generatesinglevarconstrid(duty, form) +) + c_data = ConstrData(rhs, kind, sense, inc_val, is_active, true) + constr = SingleVarConstraint(id, varid, name; constr_data = c_data) + _addconstr!(form.manager, constr) + return constr +end + function set_robust_constr_generator!(form::Formulation, kind::ConstrKind, alg::Function) constrgen = RobustConstraintsGenerator(0, kind, alg) push!(form.manager.robust_constr_generators, constrgen) @@ -621,28 +699,8 @@ function set_objective_sense!(form::Formulation, min::Bool) 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 -end - -# TODO : remove (unefficient & specific to an algorithm) -function computereducedcost(form::Formulation, varid::Id{Variable}, dualsol::DualSolution) - redcost = getperencost(form, varid) - coefficient_matrix = getcoefmatrix(form) - sign = 1 - if getobjsense(form) == MinSense - sign = -1 - end - for (constrid, dual_val) in dualsol - coeff = coefficient_matrix[constrid, varid] - redcost += sign * dual_val * coeff - end - return redcost -end - # TODO : remove (unefficient & specific to Benders) -function computereducedrhs(form::Formulation{BendersSp}, constrid::Id{Constraint}, primalsol::PrimalSolution) +function computereducedrhs(form::Formulation{BendersSp}, constrid::ConstrId, primalsol::PrimalSolution) constrrhs = getperenrhs(form,constrid) coefficient_matrix = getcoefmatrix(form) for (varid, primal_val) in primalsol @@ -654,7 +712,7 @@ end function constraint_primal(primalsol::PrimalSolution, constrid::ConstrId) val = 0.0 - for (varid, coeff) in @view getcoefmatrix(primalsol.model)[constrid, :] + for (varid, coeff) in @view getcoefmatrix(getmodel(primalsol))[constrid, :] val += coeff * primalsol[varid] end return val @@ -667,6 +725,64 @@ function push_optimizer!(form::Formulation, builder::Function) return end +############################################################################################ +# Bounds propagation +############################################################################################ + +function _update_var_cur_lb!(form::Formulation, var::Variable, lb, constrid) + cur_lb = getcurlb(form, var) + if cur_lb < lb + setcurlb!(form, var, lb) + var.moirecord.lower_bound = constrid + return true + end + return false +end + +function _update_var_cur_ub!(form::Formulation, var::Variable, ub, constrid) + cur_ub = getcurub(form, var) + if cur_ub > ub + setcurub!(form, var, ub) + var.moirecord.upper_bound = constrid + return true + end + return false +end + +function _update_bounds!(form::Formulation, var::Variable, ::Val{Greater}, rhs, constrid) + _update_var_cur_lb!(form, var, rhs, constrid) + return +end + +function _update_bounds!(form::Formulation, var::Variable, ::Val{Less}, rhs, constrid) + _update_var_cur_ub!(form, var, rhs, constrid) + return +end + +function _update_bounds!(form::Formulation, var::Variable, ::Val{Equal}, rhs, constrid) + update_lb = _update_var_cur_lb!(form, var, rhs, constrid) + update_ub = _update_var_cur_ub!(form, var, rhs, constrid) + if !update_lb || !update_ub + error("Bounds propagation determines infeasibility.") + end + return +end + +# If some new single var constraints have been added to a formulation, Coluna +# should call this method. +function bounds_propagation!(form::Formulation) + for (constrid, constr) in form.manager.single_var_constrs + var = getvar(form, constr.varid) + rhs = getcurrhs(form, constr) + _update_bounds!(form, var, Val(constr.curdata.sense), rhs, constrid) + end + return +end + +############################################################################################ +# Methods to show a formulation +############################################################################################ + function _show_obj_fun(io::IO, form::Formulation) print(io, getobjsense(form), " ") vars = filter(v -> isexplicit(form, v.first), getvars(form)) @@ -711,6 +827,27 @@ function _show_constraints(io::IO , form::Formulation) return end +function _show_singlevarconstraint(io::IO, form::Formulation, constr::SingleVarConstraint) + print(io, "| ", getname(form, constr.varid)) + op = "<=" + if getcursense(form, constr) == Equal + op = "==" + elseif getcursense(form, constr) == Greater + op = ">=" + end + println(io, " ", op, " ", getcurrhs(form, constr)) + return +end + +function _show_singlevarconstraints(io::IO, form::Formulation) + for (varid, _) in getvars(form) + for (_, constr) in getsinglevarconstrs(form, varid) + _show_singlevarconstraint(io, form, constr) + end + end + return +end + function _show_variable(io::IO, form::Formulation, var::Variable) name = getname(form, var) lb = getcurlb(form, var) @@ -739,6 +876,7 @@ function Base.show(io::IO, form::Formulation{Duty}) where {Duty <: AbstractFormD _show_obj_fun(io, form) _show_constraints(io, form) _show_variables(io, form) + _show_singlevarconstraints(io, form) end return end diff --git a/src/MathProg/manager.jl b/src/MathProg/manager.jl index 740356465..df3f4baa7 100644 --- a/src/MathProg/manager.jl +++ b/src/MathProg/manager.jl @@ -1,50 +1,58 @@ const DynSparseVector{I} = DynamicSparseArrays.PackedMemoryArray{I, Float64} -const VarDict = Dict{VarId, Variable} -const ConstrDict = Dict{ConstrId, Constraint} const VarMembership = Dict{VarId, Float64} const ConstrMembership = Dict{ConstrId, Float64} const ConstrConstrMatrix = DynamicSparseArrays.DynamicSparseMatrix{ConstrId,ConstrId,Float64} const ConstrVarMatrix = DynamicSparseArrays.DynamicSparseMatrix{ConstrId,VarId,Float64} +const VarConstrDualSolMatrix = DynamicSparseArrays.DynamicSparseMatrix{VarId,ConstrId,Tuple{Float64,ActiveBound}} const VarVarMatrix = DynamicSparseArrays.DynamicSparseMatrix{VarId,VarId,Float64} # Define the semaphore of the dynamic sparse matrix using MathProg.Id as index DynamicSparseArrays.semaphore_key(::Type{I}) where {I <: Id} = zero(I) +# The formulation manager is an internal data structure that contains & manager +# all the elements which constitute a MILP formulation: variables, constraints, +# objective constant (costs stored in variables), coefficient matrix, +# cut generators (that contain cut callbacks)... mutable struct FormulationManager - vars::VarDict - constrs::ConstrDict + vars::Dict{VarId, Variable} + constrs::Dict{ConstrId, Constraint} + single_var_constrs::Dict{SingleVarConstrId, SingleVarConstraint} + single_var_constrs_per_var::Dict{VarId, Dict{SingleVarConstrId, SingleVarConstraint}} # ids of the constraint of type : single variable >= bound objective_constant::Float64 coefficients::ConstrVarMatrix # rows = constraints, cols = variables - expressions::VarVarMatrix # cols = variables, rows = expressions primal_sols::VarVarMatrix # cols = primal solutions with varid, rows = variables primal_sols_custom_data::Dict{VarId, BD.AbstractCustomData} primal_sol_costs::DynSparseVector{VarId} # primal solutions with varid map to their cost dual_sols::ConstrConstrMatrix # cols = dual solutions with constrid, rows = constrs + dual_sols_varbounds::VarConstrDualSolMatrix # cols = dual solutions with constrid, rows = variables dual_sol_rhss::DynSparseVector{ConstrId} # dual solutions with constrid map to their rhs robust_constr_generators::Vector{RobustConstraintsGenerator} custom_families_id::Dict{DataType,Int} end function FormulationManager(; custom_families_id = Dict{BD.AbstractCustomData,Int}()) - vars = VarDict() - constrs = ConstrDict() + vars = Dict{VarId, Variable}() + constrs = Dict{ConstrId, Constraint}() return FormulationManager( vars, constrs, + Dict{SingleVarConstrId, SingleVarConstraint}(), + Dict{VarId, Dict{SingleVarConstrId, SingleVarConstraint}}(), 0.0, dynamicsparse(ConstrId, VarId, Float64), dynamicsparse(VarId, VarId, Float64; fill_mode = false), - dynamicsparse(VarId, VarId, Float64; fill_mode = false), Dict{VarId,Any}(), dynamicsparsevec(VarId[], Float64[]), dynamicsparse(ConstrId, ConstrId, Float64; fill_mode = false), + dynamicsparse(VarId, ConstrId, Tuple{Float64, ActiveBound}; fill_mode = false), dynamicsparsevec(ConstrId[], Float64[]), RobustConstraintsGenerator[], custom_families_id ) end +# Internal method to store a Variable in the formulation manager. function _addvar!(m::FormulationManager, var::Variable) if haskey(m.vars, var.id) error(string( @@ -56,6 +64,8 @@ function _addvar!(m::FormulationManager, var::Variable) return end +# Internal methods to store a Constraint or a SingleVarConstraint in the +# formulation manager. function _addconstr!(m::FormulationManager, constr::Constraint) if haskey(m.constrs, constr.id) error(string( @@ -67,6 +77,22 @@ function _addconstr!(m::FormulationManager, constr::Constraint) return end +function _addconstr!(m::FormulationManager, constr::SingleVarConstraint) + if !haskey(m.single_var_constrs_per_var, constr.varid) + m.single_var_constrs_per_var[constr.varid] = Dict{ConstrId, SingleVarConstraint}() + end + if haskey(m.single_var_constrs_per_var[constr.varid], constr.id) + name = m.single_var_constrs_per_var[constr.varid][constr.id].name + error(string( + "Constraint of id ", constr.id, "exists. Its name is ", name, + " and you want to add a constraint named ", constr.name, "." + )) + end + m.single_var_constrs[constr.id] = constr + m.single_var_constrs_per_var[constr.varid][constr.id] = constr + return +end + function Base.show(io::IO, m::FormulationManager) println(io, "FormulationManager :") println(io, "> variables : ") diff --git a/src/MathProg/reformulation.jl b/src/MathProg/reformulation.jl index 3f05969cd..962576b5e 100644 --- a/src/MathProg/reformulation.jl +++ b/src/MathProg/reformulation.jl @@ -100,6 +100,20 @@ subproblem with id `spid`. """ get_dw_pricing_sp_lb_constrid(r::Reformulation, spid::FormId) = r.dw_pricing_sp_lb[spid] +############################################################################################ +# Bounds propagation +############################################################################################ +function bounds_propagation!(reform::Reformulation) + bounds_propagation!(getmaster(reform)) + for (_, sp) in get_dw_pricing_sps(reform) + bounds_propagation!(sp) + end + for (_, sp) in get_benders_sep_sps(reform) + bounds_propagation!(sp) + end + return +end + # Following two functions are temporary, we must store a pointer to the vc # being represented by a representative vc function vc_belongs_to_formulation(form::Formulation, vc::AbstractVarConstr) diff --git a/src/MathProg/solutions.jl b/src/MathProg/solutions.jl index df8419112..a58fecaa5 100644 --- a/src/MathProg/solutions.jl +++ b/src/MathProg/solutions.jl @@ -1,26 +1,108 @@ -# new structures for solutions +# MathProg > Solutions +# Representations of the primal & dual solutions to a MILP formulation -# Constructors for Primal & Dual Solutions -const PrimalSolution{M} = Solution{M, VarId, Float64} -const DualSolution{M} = Solution{M, ConstrId, Float64} +abstract type AbstractSolution end +# Primal Solution +struct PrimalSolution{M} <: AbstractSolution + solution::Solution{M,VarId,Float64} + custom_data::Union{Nothing, BlockDecomposition.AbstractCustomData} +end + +""" + PrimalSolution( + form::AbstractFormulation, + varids::Vector{VarId}, + varvals::Vector{Float64}, + cost::Float64, + status::SolutionStatus; + custom_data::Union{Nothing, BlockDecomposition.AbstractCustomData} = nothing + ) + +Create a primal solution to the formulation `form` of cost `cost` and status `status`. +The representations of the soslution is `varids` the set of the ids of the variables +and `varvals` the values of the variables (`varvals[i]` is value of variable `varids[i]`). + +The user can also attach to the primal solution a customized representation +`custom_data`. +""" function PrimalSolution( - form::M, decisions::Vector{De}, vals::Vector{Va}, val::Float64, status::SolutionStatus, - custom_data::Union{Nothing, BD.AbstractCustomData} = nothing -) where {M<:AbstractFormulation,De,Va} - return Solution{M,De,Va}(form, decisions, vals, val, status, custom_data) + form::M, varids, varvals, cost, status; custom_data = nothing +) where {M<:AbstractFormulation} + @assert length(varids) == length(varvals) + sol = Solution{M,VarId,Float64}(form, varids, varvals, cost, status) + return PrimalSolution{M}(sol, custom_data) +end + +# Dual Solution + +# Indicate whether the active bound of a variable is the lower or the upper one. +@enum ActiveBound LOWER UPPER + +struct DualSolution{M} <: AbstractSolution + solution::Solution{M,ConstrId,Float64} + var_redcosts::Dict{VarId, Tuple{Float64,ActiveBound}} + custom_data::Union{Nothing, BlockDecomposition.AbstractCustomData} end +""" + DualSolution( + form::AbstractFormulation, + constrids::Vector{ConstrId}, + constrvals::Vector{Float64}, + varids::Vector{VarId}, + varvals::Vector{Float64}, + varactivebounds::Vector{ActiveBound}, + cost::Float64, + status::SolutionStatus; + custom_data::Union{Nothing, BlockDecomposition.AbstractColumnData} = nothing + ) + +Create a dual solution to the formulation `form` of cost `cost` and status `status`. +The first representation of the dual solution is mandatory. +It contains `constrids` the set of ids of the constraints and `constrvals` the values +of the constraints (`constrvals[i]` is dual value of `constrids[i]`). +It also contains `varvals[i]` the dual values of the bound constraint `varactivebounds[i]` of the variables `varids` +(also known as the reduced cost). + +The user can also attach to the dual solution a customized representation +`custom_data`. +""" function DualSolution( - form::M, decisions::Vector{De}, vals::Vector{Va}, val::Float64, status::SolutionStatus, - custom_data::Union{Nothing, BD.AbstractCustomData} = nothing -) where {M<:AbstractFormulation,De,Va} - return Solution{M,De,Va}(form, decisions, vals, val, status, custom_data) + form::M, constrids, constrvals, varids, varvals, varactivebounds, cost, status; + custom_data = nothing +) where {M<:AbstractFormulation} + @assert length(constrids) == length(constrvals) + @assert length(varids) == length(varvals) == length(varactivebounds) + var_redcosts = Dict{VarId, Tuple{Float64,ActiveBound}}() + for i in 1:length(varids) + var_redcosts[varids[i]] = (varvals[i],varactivebounds[i]) + end + sol = Solution{M,ConstrId,Float64}(form, constrids, constrvals, cost, status) + return DualSolution{M}(sol, var_redcosts, custom_data) end -function Base.isinteger(sol::Solution) +get_var_redcosts(s::DualSolution) = s.var_redcosts + +# Redefine methods from ColunaBase to access the formulation, the value, the +# status of a Solution, and other specific information +ColunaBase.getmodel(s::AbstractSolution) = getmodel(s.solution) +ColunaBase.getvalue(s::AbstractSolution) = getvalue(s.solution) +ColunaBase.getbound(s::AbstractSolution) = getbound(s.solution) +ColunaBase.getstatus(s::AbstractSolution) = getstatus(s.solution) +Base.length(s::AbstractSolution) = length(s.solution) +Base.get(s::AbstractSolution, id, default) = Base.get(s.solution, id, default) +Base.getindex(s::AbstractSolution, id) = Base.getindex(s.solution, id) +Base.setindex!(s::AbstractSolution, val, id) = Base.setindex!(s.solution, val, id) + +# Iterating over a PrimalSolution or a DualSolution is similar to iterating over +# ColunaBase.Solution +Base.iterate(s::AbstractSolution) = iterate(s.solution) +Base.iterate(s::AbstractSolution, state) = iterate(s.solution, state) + +function Base.isinteger(sol::PrimalSolution) for (vc_id, val) in sol - if getperenkind(sol.model, vc_id) !== Continuous && abs(round(val) - val) > 1e-5 + if getperenkind(getmodel(sol), vc_id) !== Continuous && abs(round(val) - val) > 1e-5 return false end end @@ -28,48 +110,45 @@ function Base.isinteger(sol::Solution) end function Base.isless(s1::PrimalSolution, s2::PrimalSolution) - getobjsense(s1.model) == MinSense && return s1.bound > s2.bound - return s1.bound < s2.bound + getobjsense(getmodel(s1)) == MinSense && return s1.solution.bound > s2.solution.bound + return s1.solution.bound < s2.solution.bound end function Base.isless(s1::DualSolution, s2::DualSolution) - getobjsense(s1.model) == MinSense && return s1.bound < s2.bound - return s1.bound > s2.bound -end - -function contains(sol::PrimalSolution, f::Function) - for (varid, val) in sol - f(varid) && return true - end - return false + getobjsense(getmodel(s1)) == MinSense && return s1.solution.bound < s2.solution.bound + return s1.solution.bound > s2.solution.bound end -function contains(sol::DualSolution, f::Function) - for (constrid, val) in sol - f(constrid) && return true +function contains(sol::AbstractSolution, f::Function) + for (elemid, _) in sol + f(elemid) && return true end return false end -function _assert_same_model(sols::NTuple{N, Solution{M, I, Float64}}) where {N,M,I} +function _sols_from_same_model(sols::NTuple{N, S}) where {N,S<:AbstractSolution} for i in 2:length(sols) - sols[i-1].model != sols[i].model && return false + getmodel(sols[i-1]) != getmodel(sols[i]) && return false end return true end -function Base.cat(sols::Solution{M, I, Float64}...) where {M,I} - _assert_same_model(sols) || error("Cannot concatenate solutions not attached to the same model.") +# Method `cat` is not implemented for a set of DualSolutions because @guimarqu don't know +# how to concatenate var red cost of a variable if both bounds are active in different +# solutions and because we don't need it for now. +function Base.cat(sols::PrimalSolution...) + if !_sols_from_same_model(sols) + error("Cannot concatenate solutions not attached to the same model.") + end - ids = I[] + ids = VarId[] vals = Float64[] for sol in sols, (id, value) in sol push!(ids, id) push!(vals, value) end - - return Solution{M,I,Float64}( - sols[1].model, ids, vals, sum(getvalue.(sols)), sols[1].status + return PrimalSolution( + getmodel(sols[1]), ids, vals, sum(getvalue.(sols)), getstatus(sols[1]) ) end @@ -84,7 +163,7 @@ end function Base.show(io::IO, solution::DualSolution{M}) where {M} println(io, "Dual solution") for (constrid, value) in solution - println(io, "| ", getname(solution.model, constrid), " = ", value) + println(io, "| ", getname(getmodel(solution), constrid), " = ", value) end Printf.@printf(io, "└ value = %.2f \n", getvalue(solution)) end @@ -92,7 +171,18 @@ end function Base.show(io::IO, solution::PrimalSolution{M}) where {M} println(io, "Primal solution") for (varid, value) in solution - println(io, "| ", getname(solution.model, varid), " = ", value) + println(io, "| ", getname(getmodel(solution), varid), " = ", value) end Printf.@printf(io, "└ value = %.2f \n", getvalue(solution)) end + +# Following methods are needed by Benders +# TODO : check if we can remove them during refactoring of Benders +# not performant +Base.haskey(s::AbstractSolution, key) = haskey(s.solution, key) +# we can't filter the constraints, the variables, and the custom data. +function Base.filter(f::Function, s::DualSolution) + return DualSolution( + filter(f, s.solution), s.var_redcosts, s.custom_data + ) +end \ No newline at end of file diff --git a/src/MathProg/types.jl b/src/MathProg/types.jl index cfe2c9631..3c4bee17e 100644 --- a/src/MathProg/types.jl +++ b/src/MathProg/types.jl @@ -1,8 +1,4 @@ abstract type AbstractVarConstr end -abstract type AbstractVarConstrId end -abstract type AbstractState end -abstract type AstractMoiDef end -abstract type AbstractMembership end abstract type AbstractVcData end abstract type AbstractOptimizer end @@ -13,9 +9,7 @@ struct Dual <: Coluna.AbstractDualSpace end struct MinSense <: Coluna.AbstractMinSense end struct MaxSense <: Coluna.AbstractMaxSense end -# # Duties for variables and constraints -# """ Duty{Variable} @@ -34,7 +28,7 @@ If a duty `Duty1` inherits from `Duty2`, then ```jldoctest julia> Duty1 <= Duty2 true -```` +``` """ struct Duty{VC <: AbstractVarConstr} <: NestedEnum value::UInt @@ -56,8 +50,6 @@ end # TODO remove following exported_enum @exported_enum FormulationPhase HybridPhase PurePhase1 PurePhase2 # TODO : remove from Benders -#@exported_enum VcSelectionCriteria Static Dynamic Delayed Artificial Implicit Explicit # Not used -#@exported_enum SolutionMethod DirectMip DantzigWolfeDecomposition BendersDecomposition # Not used const FormId = Int16 diff --git a/src/MathProg/varconstr.jl b/src/MathProg/varconstr.jl index 447b2453e..ac1607a62 100644 --- a/src/MathProg/varconstr.jl +++ b/src/MathProg/varconstr.jl @@ -1,7 +1,20 @@ + +# There are two levels of data for each element of a formulation (i.e. variables and +# constraints). +# The first level is called "peren" (for perennial). It contains data that won't change for +# almost all the optimisation (e.g. the original cost of a variable, the original sense of +# constraint...). Coluna provides methods to set these data because it can ease the setup +# of a formulation. Algorithm designers are free to use these method at their own risk. +# The second level is called "cur" (for current). It describes the current state of each +# element of the formulation. + getid(vc::AbstractVarConstr) = vc.id -getmoirecord(vc::AbstractVarConstr) = vc.moirecord getoriginformuid(vc::AbstractVarConstr) = getoriginformuid(getid(vc)) +# no moi record for a single variable constraint +getmoirecord(vc::Variable) = vc.moirecord +getmoirecord(vc::Constraint) = vc.moirecord + # Variables ## Cost """ @@ -11,7 +24,7 @@ getoriginformuid(vc::AbstractVarConstr) = getoriginformuid(getid(vc)) Return the cost as defined by the user of a variable in a formulation. """ getperencost(form::Formulation, varid::VarId) = getperencost(form, getvar(form, varid)) -getperencost(form::Formulation, var::Variable) = var.perendata.cost +getperencost(::Formulation, var::Variable) = var.perendata.cost """ getcurcost(formulation, variable) @@ -20,7 +33,7 @@ getperencost(form::Formulation, var::Variable) = var.perendata.cost Return the current cost of the variable in the formulation. """ getcurcost(form::Formulation, varid::VarId) = getcurcost(form, getvar(form, varid)) -getcurcost(form::Formulation, var::Variable) = var.curdata.cost +getcurcost(::Formulation, var::Variable) = var.curdata.cost """ setperencost!(formulation, variable, cost) @@ -63,7 +76,7 @@ end Return the lower bound as defined by the user of a variable in a formulation. """ getperenlb(form::Formulation, varid::VarId) = getperenlb(form, getvar(form, varid)) -getperenlb(form::Formulation, var::Variable) = var.perendata.lb +getperenlb(::Formulation, var::Variable) = var.perendata.lb """ getcurlb(formulation, varid) @@ -72,7 +85,7 @@ getperenlb(form::Formulation, var::Variable) = var.perendata.lb Return the current lower bound of a variable in a formulation. """ getcurlb(form::Formulation, varid::VarId) = getcurlb(form, getvar(form, varid)) -getcurlb(form::Formulation, var::Variable) = var.curdata.lb +getcurlb(::Formulation, var::Variable) = var.curdata.lb """ setcurlb!(formulation, varid, lb::Float64) @@ -99,7 +112,7 @@ setcurlb!(form::Formulation, varid::VarId, lb::Float64) = setcurlb!(form, getva Return the upper bound as defined by the user of a variable in a formulation. """ getperenub(form::Formulation, varid::VarId) = getperenub(form, getvar(form, varid)) -getperenub(form::Formulation, var::Variable) = var.perendata.ub +getperenub(::Formulation, var::Variable) = var.perendata.ub """ getcurub(formulation, varid) @@ -108,7 +121,7 @@ getperenub(form::Formulation, var::Variable) = var.perendata.ub Return the current upper bound of a variable in a formulation. """ getcurub(form::Formulation, varid::VarId) = getcurub(form, getvar(form, varid)) -getcurub(form::Formulation, var::Variable) = var.curdata.ub +getcurub(::Formulation, var::Variable) = var.curdata.ub """ setcurub!(formulation, varid, ub::Float64) @@ -137,7 +150,8 @@ setcurub!(form::Formulation, varid::VarId, ub::Float64) = setcurub!(form, getvar Return the right-hand side as defined by the user of a constraint in a formulation. """ getperenrhs(form::Formulation, constrid::ConstrId) = getperenrhs(form, getconstr(form, constrid)) -getperenrhs(form::Formulation, constr::Constraint) = constr.perendata.rhs +getperenrhs(form::Formulation, constrid::SingleVarConstrId) = getperenrhs(form, getconstr(form, constrid)) +getperenrhs(::Formulation, constr::AbstractConstraint) = constr.perendata.rhs """ getcurrhs(formulation, constraint) @@ -146,7 +160,8 @@ getperenrhs(form::Formulation, constr::Constraint) = constr.perendata.rhs Return the current right-hand side of a constraint in a formulation. """ getcurrhs(form::Formulation, constrid::ConstrId) = getcurrhs(form, getconstr(form, constrid)) -getcurrhs(form::Formulation, constr::Constraint) = constr.curdata.rhs +getcurrhs(form::Formulation, constrid::SingleVarConstrId) = getcurrhs(form, getconstr(form, constrid)) +getcurrhs(form::Formulation, constr::AbstractConstraint) = constr.curdata.rhs """ setperenrhs!(formulation, constr, rhs) @@ -155,11 +170,15 @@ getcurrhs(form::Formulation, constr::Constraint) = constr.curdata.rhs Set the perennial rhs of a constraint in a formulation. Change is propagated to the current rhs of the constraint. """ -function setperenrhs!(form::Formulation, constr::Constraint, rhs) +function setperenrhs!(form::Formulation, constr::AbstractConstraint, rhs) constr.perendata.rhs = rhs return setcurrhs!(form, constr, rhs) end -setperenrhs!(form::Formulation, constrid::ConstrId, rhs) = setperenrhs!(form, getconstr(form, constrid), rhs) +setperenrhs!(form::Formulation, constrid::ConstrId, rhs) = + setperenrhs!(form, getconstr(form, constrid), rhs) + +setperenrhs!(form::Formulation, constrid::SingleVarConstrId, rhs) = + setperenrhs!(form, getconstr(form, constrid), rhs) """ setcurrhs(formulation, constraint, rhs::Float64) @@ -168,6 +187,9 @@ setperenrhs!(form::Formulation, constrid::ConstrId, rhs) = setperenrhs!(form, ge Set the current right-hand side of a constraint in a formulation. If the constraint is active and explicit, this change is buffered before application to the subsolver. + +**Warning** : if you change the rhs of a single variable constraint, make sure that you +perform bound propagation before calling the subsolver of the formulation. """ function setcurrhs!(form::Formulation, constr::Constraint, rhs::Float64) constr.curdata.rhs = rhs @@ -176,7 +198,15 @@ function setcurrhs!(form::Formulation, constr::Constraint, rhs::Float64) end return end -setcurrhs!(form::Formulation, constrid::ConstrId, rhs::Float64) = setcurrhs!(form, getconstr(form, constrid), rhs) + +setcurrhs!(form::Formulation, constrid::ConstrId, rhs::Float64) = + setcurrhs!(form, getconstr(form, constrid), rhs) + +setcurrhs!(form::Formulation, constr::SingleVarConstraint, rhs::Float64) = + constr.curdata.rhs = rhs + +setcurrhs!(form::Formulation, constrid::SingleVarConstrId, rhs::Float64) = + setcurrhs!(form, getconstr(form, constrid), rhs) # Variable & Constraints ## kind @@ -196,9 +226,10 @@ Kinds of a constraint (`enum ConstrKind`) are : The kind of a constraint cannot change. """ getperenkind(form::Formulation, varid::VarId) = getperenkind(form, getvar(form, varid)) -getperenkind(form::Formulation, var::Variable) = var.perendata.kind +getperenkind(::Formulation, var::Variable) = var.perendata.kind getperenkind(form::Formulation, constrid::ConstrId) = getperenkind(form, getconstr(form, constrid)) -getperenkind(form::Formulation, constr::Constraint) = constr.perendata.kind +getperenkind(form::Formulation, constrid::SingleVarConstrId) = getperenkind(form, getconstr(form, constrid)) +getperenkind(::Formulation, constr::AbstractConstraint) = constr.perendata.kind """ getcurkind(formulation, variable) @@ -207,7 +238,20 @@ getperenkind(form::Formulation, constr::Constraint) = constr.perendata.kind Return the current kind of a variable in a formulation. """ getcurkind(form::Formulation, varid::VarId) = getcurkind(form, getvar(form, varid)) -getcurkind(form::Formulation, var::Variable) = var.curdata.kind +getcurkind(::Formulation, var::Variable) = var.curdata.kind + +""" + setperenkind!(formulation, variable, kind) + setperenkind!(formulation, varid, kind) + +Set the perennial kind of a variable in a formulation. +This change is then propagated to the current kind of the variable. +""" +function setperenkind!(form::Formulation, var::Variable, kind::VarKind) + var.perendata.kind = kind + return setcurkind!(form, var, kind) +end +setperenkind!(form::Formulation, varid::VarId, kind::VarKind) = setperenkind!(form, getvar(form, varid), kind) """ setcurkind!(formulation, variable, kind::VarKind) @@ -226,7 +270,6 @@ function setcurkind!(form::Formulation, var::Variable, kind::VarKind) end setcurkind!(form::Formulation, varid::VarId, kind::VarKind) = setcurkind!(form, getvar(form, varid), kind) - ## sense function _senseofvar(lb::Float64, ub::Float64) lb >= 0 && return Positive @@ -248,7 +291,8 @@ The perennial sense of a variable depends on its perennial bounds. getperensense(form::Formulation, varid::VarId) = getperensense(form, getvar(form, varid)) getperensense(form::Formulation, var::Variable) = _senseofvar(getperenlb(form, var), getperenub(form, var)) getperensense(form::Formulation, constrid::ConstrId) = getperensense(form, getconstr(form, constrid)) -getperensense(form::Formulation, constr::Constraint) = constr.perendata.sense +getperensense(form::Formulation, constrid::SingleVarConstrId) = getperensense(form, getconstr(form, constrid)) +getperensense(::Formulation, constr::AbstractConstraint) = constr.perendata.sense """ getcursense(formulation, varconstr) @@ -257,10 +301,11 @@ getperensense(form::Formulation, constr::Constraint) = constr.perendata.sense Return the current sense of a variable or a constraint in a formulation. The current sense of a variable depends on its current bounds. """ -getcursense(form::Formulation, varid::VarId) = getcursense(form, getconstr(form, varid)) +getcursense(form::Formulation, varid::VarId) = getcursense(form, getvar(form, varid)) getcursense(form::Formulation, var::Variable) = _senseofvar(getcurlb(form, var), getcurub(form, var)) getcursense(form::Formulation, constrid::ConstrId) = getcursense(form, getconstr(form, constrid)) -getcursense(form::Formulation, constr::Constraint) = constr.curdata.sense +getcursense(form::Formulation, constrid::SingleVarConstrId) = getcursense(form, getconstr(form, constrid)) +getcursense(::Formulation, constr::AbstractConstraint) = constr.curdata.sense """ setperensense!(form, constr, sense) @@ -268,12 +313,20 @@ getcursense(form::Formulation, constr::Constraint) = constr.curdata.sense Set the perennial sense of a constraint in a formulation. Change is propagated to the current sense of the constraint. + +**Warning** : if you set the sense of a single var constraint, make sure you perform bound +propagation before calling the subsolver of the formulation. """ -function setperensense!(form::Formulation, constr::Constraint, sense::ConstrSense) +function setperensense!(form::Formulation, constr::AbstractConstraint, sense::ConstrSense) constr.perendata.sense = sense return setcursense!(form, constr, sense) end -setperensense!(form::Formulation, constrid::ConstrId, sense::ConstrSense) = setperensense!(form, getconstr(form, constrid), sense) + +setperensense!(form::Formulation, constrid::ConstrId, sense::ConstrSense) = + setperensense!(form, getconstr(form, constrid), sense) + +setperensense!(form::Formulation, constrid::SingleVarConstrId, sense::ConstrSense) = + setperensense!(form, getconstr(form, constrid), sense) """ setcursense!(formulation, constr, sense::ConstrSense) @@ -283,6 +336,9 @@ Set the current sense of a constraint in a formulation. This method is not applicable to variables because the sense of a variable depends on its bounds. + +**Warning** : if you set the sense of a single var constraint, make sure you perform bound +propagation before calling the subsolver of the formulation. """ function setcursense!(form::Formulation, constr::Constraint, sense::ConstrSense) constr.curdata.sense = sense @@ -292,9 +348,14 @@ function setcursense!(form::Formulation, constr::Constraint, sense::ConstrSense) return end -function setcursense!(form::Formulation, constrid::ConstrId, sense::ConstrSense) - return setcursense!(form, getconstr(form, constrid), sense) -end +setcursense!(form::Formulation, constrid::ConstrId, sense::ConstrSense) = + setcursense!(form, getconstr(form, constrid), sense) + +setcursense!(::Formulation, constr::SingleVarConstraint, sense::ConstrSense) = + constr.curdata.sense = sense + +setcursense!(form::Formulation, constrid::SingleVarConstrId, sense::ConstrSense) = + setcursense!(form, getconstr(form, constrid), sense) ## inc_val """ @@ -306,9 +367,10 @@ The incumbent value is the primal value associated to a variable or the dual val a constraint. """ getperenincval(form::Formulation, varid::VarId) = getperenincval(form, getvar(form, varid)) -getperenincval(form::Formulation, var::Variable) = var.perendata.inc_val +getperenincval(::Formulation, var::Variable) = var.perendata.inc_val getperenincval(form::Formulation, constrid::ConstrId) = getperenincval(form, getconstr(form, constrid)) -getperenincval(form::Formulation, constr::Constraint) = constr.perendata.inc_val +getperenincval(form::Formulation, constrid::SingleVarConstrId) = getperenincval(form, getconstr(form, constrid)) +getperenincval(::Formulation, constr::AbstractConstraint) = constr.perendata.inc_val """ getcurincval(formulation, varconstrid) @@ -317,25 +379,27 @@ getperenincval(form::Formulation, constr::Constraint) = constr.perendata.inc_val Return the current incumbent value of a variable or a constraint in a formulation. """ getcurincval(form::Formulation, varid::VarId) = getcurincval(form, getvar(form, varid)) -getcurincval(form::Formulation, var::Variable) = var.curdata.inc_val +getcurincval(::Formulation, var::Variable) = var.curdata.inc_val getcurincval(form::Formulation, constrid::ConstrId) = getcurincval(form, getconstr(form, constrid)) -getcurincval(form::Formulation, constr::Constraint) = constr.curdata.inc_val +getcurincval(form::Formulation, constrid::SingleVarConstrId) = getcurincval(form, getconstr(form, constrid)) +getcurincval(::Formulation, constr::AbstractConstraint) = constr.curdata.inc_val """ setcurincval!(formulation, varconstrid, value::Real) Set the current incumbent value of a variable or a constraint in a formulation. """ -function setcurincval!(form::Formulation, var::Variable, inc_val::Real) +setcurincval!(::Formulation, var::Variable, inc_val::Real) = var.curdata.inc_val = inc_val - return -end -setcurincval!(form::Formulation, varid::VarId, inc_val::Real) = setcurincval!(form, getvar(form, varid), inc_val) -function setcurincval!(form::Formulation, constr::Constraint, inc_val::Real) + +setcurincval!(form::Formulation, varid::VarId, inc_val) = + setcurincval!(form, getvar(form, varid), inc_val) + +setcurincval!(::Formulation, constr::AbstractConstraint, inc_val::Real) = constr.curdata.inc_val = inc_val - return -end -setcurincval!(form::Formulation, constrid::ConstrId, inc_val::Real) = setcurincval!(form, getconstr(form, constrid), inc_val) + +setcurincval!(form::Formulation, constrid::ConstrId, inc_val) = + setcurincval!(form, getconstr(form, constrid), inc_val) ## active """ @@ -348,9 +412,10 @@ deletion of the variable by deativate it. This allows you to keep the variable i to reactivate it later. """ isperenactive(form::Formulation, varid::VarId) = isperenactive(form, getvar(form, varid)) -isperenactive(form::Formulation, var::Variable) = var.perendata.is_active +isperenactive(::Formulation, var::Variable) = var.perendata.is_active isperenactive(form::Formulation, constrid::ConstrId) = isperenactive(form, getconstr(form, constrid)) -isperenactive(form::Formulation, constr::Constraint) = constr.perendata.is_active +isperenactive(form::Formulation, constrid::SingleVarConstrId) = isperenactive(form, getconstr(form, constrid)) +isperenactive(::Formulation, constr::AbstractConstraint) = constr.perendata.is_active """ iscuractive(formulation, varconstrid) @@ -359,10 +424,12 @@ isperenactive(form::Formulation, constr::Constraint) = constr.perendata.is_activ Return `true` if the variable or the constraint is currently active; `false` otherwise. """ iscuractive(form::Formulation, varid::VarId) = iscuractive(form, getvar(form, varid)) -iscuractive(form::Formulation, var::Variable) = var.curdata.is_active +iscuractive(::Formulation, var::Variable) = var.curdata.is_active iscuractive(form::Formulation, constrid::ConstrId) = iscuractive(form, getconstr(form, constrid)) -iscuractive(form::Formulation, constr::Constraint) = constr.curdata.is_active +iscuractive(form::Formulation, constrid::SingleVarConstrId) = iscuractive(form, getconstr(form, constrid)) +iscuractive(::Formulation, constr::AbstractConstraint) = constr.curdata.is_active +## activate! function _activate!(form::Formulation, varconstr::AbstractVarConstr) if isexplicit(form, varconstr) && !iscuractive(form, varconstr) add!(form.buffer, getid(varconstr)) @@ -410,6 +477,7 @@ function activate!(form::Formulation, f::Function) return end +## deactivate! function _deactivate!(form::Formulation, varconstr::AbstractVarConstr) if isexplicit(form, varconstr) && iscuractive(form, varconstr) remove!(form.buffer, getid(varconstr)) @@ -455,19 +523,21 @@ end ## delete """ - delete!(formulation, varconstrid) delete!(formulation, varconstr) + delete!(formulation, varconstrid) Delete a variable or a constraint from a formulation. """ -function Base.delete!(form::Formulation, varid::VarId) +function Base.delete!(form::Formulation, var::Variable) + varid = getid(var) delete!(form.manager.vars, varid) - delete!(form.buffer.var_buffer.added, varid) + remove!(form.buffer, varid) return end -Base.delete!(form::Formulation, var::Variable) = delete!(form, getid(var)) +Base.delete!(form::Formulation, id::VarId) = delete!(form, getvar(form, id)) -function Base.delete!(form::Formulation, constrid::ConstrId) +function Base.delete!(form::Formulation, constr::Constraint) + constrid = getid(constr) coefmatrix = getcoefmatrix(form) varids = VarId[] for (varid, _) in @view coefmatrix[constrid, :] @@ -476,11 +546,19 @@ function Base.delete!(form::Formulation, constrid::ConstrId) for varid in varids coefmatrix[constrid, varid] = 0.0 end - delete!(form.buffer.constr_buffer.added, constrid) delete!(form.manager.constrs, constrid) + remove!(form.buffer, constrid) + return +end +Base.delete!(form::Formulation, id::ConstrId) = delete!(form, getconstr(form, id)) + +function Base.delete!(form::Formulation, constr::SingleVarConstraint) + constrid = getid(constr) + delete!(form.manager.single_var_constrs, constrid) + delete!(form.manager.single_var_constrs_per_var[constr.varid], constrid) return end -Base.delete!(form::Formulation, constr::Constraint) = delete!(form, getid(constr)) +Base.delete!(form::Formulation, id::SingleVarConstrId) = delete!(form, getconstr(form, id)) ## explicit """ @@ -490,9 +568,11 @@ Base.delete!(form::Formulation, constr::Constraint) = delete!(form, getid(constr Return `true` if a variable or a constraint is explicit in a formulation; `false` otherwise. """ isexplicit(form::Formulation, varid::VarId) = isexplicit(form, getvar(form, varid)) -isexplicit(form::Formulation, var::Variable) = var.perendata.is_explicit +isexplicit(::Formulation, var::Variable) = var.perendata.is_explicit isexplicit(form::Formulation, constrid::ConstrId) = isexplicit(form, getconstr(form, constrid)) -isexplicit(form::Formulation, constr::Constraint) = constr.perendata.is_explicit +isexplicit(::Formulation, constr::Constraint) = constr.perendata.is_explicit +isexplicit(::Formulation, ::SingleVarConstrId) = true +isexplicit(::Formulation, ::SingleVarConstraint) = true ## name """ @@ -502,9 +582,10 @@ isexplicit(form::Formulation, constr::Constraint) = constr.perendata.is_explicit Return the name of a variable or a constraint in a formulation. """ getname(form::Formulation, varid::VarId) = getvar(form, varid).name -getname(form::Formulation, var::Variable) = var.name +getname(::Formulation, var::Variable) = var.name getname(form::Formulation, constrid::ConstrId) = getconstr(form, constrid).name -getname(form::Formulation, constr::Constraint) = constr.name +getname(form::Formulation, constrid::SingleVarConstrId) = getconstr(form, constrid).name +getname(::Formulation, constr::AbstractConstraint) = constr.name ## branching_priority """ @@ -514,9 +595,9 @@ getname(form::Formulation, constr::Constraint) = constr.name Return the branching priority of a variable """ getbranchingpriority(form::Formulation, varid::VarId) = getvar(form, varid).branching_priority -getbranchingpriority(form::Formulation, var::Variable) = var.branching_priority +getbranchingpriority(::Formulation, var::Variable) = var.branching_priority -# Reset +# Reset (this method is used only in tests... @guimarqu doesn't know if we should keep it) """ reset!(form, var) reset!(form, varid) diff --git a/src/MathProg/variable.jl b/src/MathProg/variable.jl index bf00995c8..04c31b79e 100644 --- a/src/MathProg/variable.jl +++ b/src/MathProg/variable.jl @@ -51,11 +51,13 @@ Structure to hold the pointers to the MOI representation of a Coluna Variable. mutable struct MoiVarRecord index::MoiVarIndex bounds::MoiVarBound + lower_bound::Union{Nothing,Id{<:AbstractVarConstr}} + upper_bound::Union{Nothing,Id{<:AbstractVarConstr}} kind::MoiVarKind end function MoiVarRecord(;index::MoiVarIndex = MoiVarIndex()) - return MoiVarRecord(index, MoiVarBound(), MoiVarKind()) + return MoiVarRecord(index, MoiVarBound(), nothing, nothing, MoiVarKind()) end getindex(record::MoiVarRecord) = record.index @@ -72,7 +74,7 @@ setkind!(record::MoiVarRecord, kind::MoiVarKind) = record.kind = kind Representation of a variable in Coluna. """ mutable struct Variable <: AbstractVarConstr - id::Id{Variable} + id::Id{Variable,:usual} name::String perendata::VarData curdata::VarData @@ -81,7 +83,7 @@ mutable struct Variable <: AbstractVarConstr custom_data::Union{Nothing, BD.AbstractCustomData} end -const VarId = Id{Variable} +const VarId = Id{Variable,:usual} getid(var::Variable) = var.id diff --git a/src/MathProg/vcids.jl b/src/MathProg/vcids.jl index 587b3b77c..3520942ff 100644 --- a/src/MathProg/vcids.jl +++ b/src/MathProg/vcids.jl @@ -10,7 +10,7 @@ It is composed by the following uids: 4. `proc_uid`: Number of the process where it was generated For a origin jump var/constr the origin_form_uid is the jump model while the assigned_form_uid_in_reformulation is the spform for a pure spform and the master for a pure master var. For a added var/constr the origin_form_uid is where is was created : for instance a master column 's orginal formulation is the subproblem for which it was a solution and is assigned formulation is the master program.Number of the process where it was generated """ -struct Id{VC <: AbstractVarConstr} +struct Id{VC <: AbstractVarConstr,F} # F is a flag to differenciate ConstrId & SingleVarConstrId duty::Duty{VC} uid::Int32 origin_form_uid::FormId @@ -20,62 +20,48 @@ struct Id{VC <: AbstractVarConstr} _hash::Int end -function _create_hash(uid, origin_form_uid, proc_uid) - return ( - uid * MAX_NB_FORMULATIONS * MAX_NB_PROCESSES - + origin_form_uid * MAX_NB_PROCESSES - + proc_uid - ) -end - -function Id{VC}(duty::Duty{VC}, uid, origin_form_uid, assigned_form_uid, custom_family_id) where {VC} - proc_uid = Distributed.myid() - Id{VC}(duty, uid, origin_form_uid, assigned_form_uid, proc_uid, custom_family_id, _create_hash(uid, origin_form_uid, proc_uid)) -end - -function Id{VC}(duty::Duty{VC}, uid, origin_form_uid) where {VC} - proc_uid = Distributed.myid() - Id{VC}(duty, uid, origin_form_uid, origin_form_uid, proc_uid, -1, _create_hash(uid, origin_form_uid, proc_uid)) -end +_create_hash(uid, origin_form_uid, proc_uid) = + uid * MAX_NB_FORMULATIONS * MAX_NB_PROCESSES + + origin_form_uid * MAX_NB_PROCESSES + proc_uid -function Id{VC}(duty::Duty{VC}, uid, origin_form_uid, custom_family_id) where {VC} +function Id{VC,F}(duty::Duty{VC}, uid, origin_form_uid, custom_family_id) where {VC,F} proc_uid = Distributed.myid() - Id{VC}(duty, uid, origin_form_uid, origin_form_uid, proc_uid, custom_family_id, _create_hash(uid, origin_form_uid, proc_uid)) + Id{VC,F}(duty, uid, origin_form_uid, origin_form_uid, proc_uid, custom_family_id, _create_hash(uid, origin_form_uid, proc_uid)) end -function Id{VC}(duty::Duty{VC}, id::Id{VC}, assigned_form_uid_in_reformulation) where {VC} - Id{VC}(duty, id.uid, id.origin_form_uid, assigned_form_uid_in_reformulation, id.proc_uid, id.custom_family_id, id._hash) +function Id{VC,F}(duty::Duty{VC}, id::Id{VC,F}, assigned_form_uid_in_reformulation) where {VC,F} + Id{VC,F}(duty, id.uid, id.origin_form_uid, assigned_form_uid_in_reformulation, id.proc_uid, id.custom_family_id, id._hash) end -function Id{VC}(duty::Duty{VC}, id::Id{VC}; custom_family_id = id.custom_family_id) where {VC} - Id{VC}(duty, id.uid, id.origin_form_uid, id.assigned_form_uid_in_reformulation, id.proc_uid, custom_family_id, id._hash) +function Id{VC,F}(duty::Duty{VC}, id::Id{VC,F}; custom_family_id = id.custom_family_id) where {VC,F} + Id{VC,F}(duty, id.uid, id.origin_form_uid, id.assigned_form_uid_in_reformulation, id.proc_uid, custom_family_id, id._hash) end Base.hash(a::Id, h::UInt) = hash(a._hash, h) -Base.isequal(a::Id{VC}, b::Id{VC}) where {VC} = Base.isequal(a._hash, b._hash) +Base.isequal(a::Id{VC,F}, b::Id{VC,F}) where {VC,F} = Base.isequal(a._hash, b._hash) Base.isequal(a::Int, b::Id) = Base.isequal(a, b._hash) Base.isequal(a::Id, b::Int) = Base.isequal(a._hash, b) -Base.isless(a::Id{VC}, b::Id{VC}) where {VC} = Base.isless(a._hash, b._hash) -Base.zero(I::Type{Id{VC}}) where {VC} = I(Duty{VC}(0), -1, -1, -1, -1, -1, -1) +Base.isless(a::Id{VC,F}, b::Id{VC,F}) where {VC,F} = Base.isless(a._hash, b._hash) +Base.zero(I::Type{Id{VC,F}}) where {VC,F} = I(Duty{VC}(0), -1, -1, -1, -1, -1, -1) -Base.:(<)(a::Id{VC}, b::Id{VC}) where {VC} = a._hash < b._hash -Base.:(<=)(a::Id{VC}, b::Id{VC}) where {VC} = a._hash <= b._hash -Base.:(==)(a::Id{VC}, b::Id{VC}) where {VC} = a._hash == b._hash -Base.:(>)(a::Id{VC}, b::Id{VC}) where {VC} = a._hash > b._hash -Base.:(>=)(a::Id{VC}, b::Id{VC}) where {VC} = a._hash >= b._hash +Base.:(<)(a::Id{VC,F}, b::Id{VC,F}) where {VC,F} = a._hash < b._hash +Base.:(<=)(a::Id{VC,F}, b::Id{VC,F}) where {VC,F} = a._hash <= b._hash +Base.:(==)(a::Id{VC,F}, b::Id{VC,F}) where {VC,F} = a._hash == b._hash +Base.:(>)(a::Id{VC,F}, b::Id{VC,F}) where {VC,F} = a._hash > b._hash +Base.:(>=)(a::Id{VC,F}, b::Id{VC,F}) where {VC,F} = a._hash >= b._hash getuid(id::Id) = id.uid -getduty(vcid::Id{VC}) where {VC} = vcid.duty +getduty(vcid::Id{VC,F}) where {VC,F} = vcid.duty getoriginformuid(id::Id) = id.origin_form_uid getassignedformuid(id::Id) = id.assigned_form_uid_in_reformulation getprocuid(id::Id) = id.proc_uid getsortuid(id::Id) = getuid(id) + 1000000 * getoriginformuid(id) -function Base.show(io::IO, id::Id{T}) where {T} - print(io, T, "#", +function Base.show(io::IO, id::Id{T,F}) where {T,F} + print(io, T, "#(", F ,")", "u", id.uid, "f", id.origin_form_uid, "a", id.assigned_form_uid_in_reformulation, - "p", id.proc_uid , + "p", id.proc_uid, "h", id._hash) end diff --git a/src/decomposition.jl b/src/decomposition.jl index a91245235..e15d98aff 100644 --- a/src/decomposition.jl +++ b/src/decomposition.jl @@ -83,6 +83,7 @@ function instantiatesp!( end # Master of Dantzig-Wolfe decomposition +# We clone the variables and the single variable constraints at the same time function instantiate_orig_vars!( masterform::Formulation{DwMaster}, origform::Formulation, @@ -92,11 +93,12 @@ function instantiate_orig_vars!( vars_per_ann = annotations.vars_per_ann for (ann, vars) in vars_per_ann formtype = BD.getformulation(ann) - dectype = BD.getdecomposition(ann) if formtype <: BD.Master - for (id, var) in vars - #duty, explicit = _varexpduty(DwMaster, formtype, dectype) + for (_, var) in vars clonevar!(origform, masterform, masterform, var, MasterPureVar, is_explicit = true) + for (_, constr) in getsinglevarconstrs(origform, getid(var)) + clonesinglevarconstr!(origform, masterform, masterform, constr, MasterPureConstr) + end end end end @@ -112,7 +114,7 @@ function instantiate_orig_constrs!( ) !haskey(annotations.constrs_per_ann, mast_ann) && return constrs = annotations.constrs_per_ann[mast_ann] - for (id, constr) in constrs + for (_, constr) in constrs cloneconstr!( origform, masterform, masterform, constr, MasterMixedConstr, loc_art_var_abs_cost = env.params.local_art_var_cost @@ -195,11 +197,14 @@ function instantiate_orig_vars!( !haskey(annotations.vars_per_ann, sp_ann) && return vars = annotations.vars_per_ann[sp_ann] masterform = spform.parent_formulation - for (id, var) in vars + for (varid, var) in vars # An original variable annotated in a subproblem is a DwSpPricingVar clonevar!(origform, spform, spform, var, DwSpPricingVar, is_explicit = true) - clonevar!(origform, masterform, spform, var, MasterRepPricingVar, - is_explicit = false)#, members = getcoefmatrix(origform)[:,id]) + for (_, constr) in getsinglevarconstrs(origform, varid) + clonesinglevarconstr!(origform, spform, spform, constr, DwSpPureConstr) + end + + clonevar!(origform, masterform, spform, var, MasterRepPricingVar, is_explicit = false) end return end @@ -248,7 +253,6 @@ function _dutyexpofbendmastvar( end # Master of Benders decomposition - function instantiate_orig_vars!( masterform::Formulation{BendersMaster}, origform::Formulation{Original}, @@ -257,8 +261,11 @@ function instantiate_orig_vars!( ) !haskey(annotations.vars_per_ann, mast_ann) && return vars = annotations.vars_per_ann[mast_ann] - for (_, var) in vars - clonevar!(origform, masterform, masterform, var, MasterPureVar, is_explicit = true) + for (varid, var) in vars + clonevar!(origform, masterform, masterform, var, MasterPureVar, is_explicit = true) + for (_, constr) in getsinglevarconstrs(origform, varid) + clonesinglevarconstr!(origform, masterform, masterform, constr, MasterPureConstr) + end end return end @@ -300,7 +307,7 @@ function create_side_vars_constrs!( ub = getperenub(spform, nu_var), kind = Continuous, is_explicit = true, - id = Id{Variable}(MasterBendSecondStageCostVar, getid(nu_var), getuid(masterform)) + id = VarId(MasterBendSecondStageCostVar, getid(nu_var), getuid(masterform)) ) end return @@ -316,8 +323,11 @@ function instantiate_orig_vars!( ) if haskey(annotations.vars_per_ann, sp_ann) vars = annotations.vars_per_ann[sp_ann] - for (_, var) in vars + for (varid, var) in vars clonevar!(origform, spform, spform, var, BendSpSepVar, cost = 0.0) + for (_, constr) in getsinglevarconstrs(origform, varid) + clonesinglevarconstr!(origform, spform, spform, constr, BendSpPureConstr) + end end end return @@ -375,7 +385,7 @@ function create_side_vars_constrs!( ub = getcurub(origform, var), kind = Continuous, is_explicit = true, - id = Id{Variable}(BendSpPosSlackFirstStageVar, varid, getuid(masterform)) + id = VarId(BendSpPosSlackFirstStageVar, varid, getuid(masterform)) ) name = string("μ⁻[", split(getname(origform, var), "[")[end], "]") @@ -559,10 +569,14 @@ function reformulate!(prob::Problem, annotations::Annotations, env::Env) # Once the original formulation built, we close the "fill mode" of the # coefficient matrix which is a super fast writing mode compared to the default # writing mode of the dynamic sparse matrix. - if getcoefmatrix(prob.original_formulation).fillmode - closefillmode!(getcoefmatrix(prob.original_formulation)) + origform = get_original_formulation(prob) + if getcoefmatrix(origform).fillmode + closefillmode!(getcoefmatrix(origform)) end + # Update the bounds of the original formulation before reformulating + MathProg.bounds_propagation!(origform) + decomposition_tree = annotations.tree if decomposition_tree !== nothing check_annotations(prob, annotations) diff --git a/src/optimize.jl b/src/optimize.jl index cd35a3a4d..15c7b1b25 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -69,6 +69,7 @@ end function optimize!( reform::MathProg.Reformulation, env::Env, initial_primal_bound, initial_dual_bound ) + MathProg.bounds_propagation!(reform) master = getmaster(reform) initstate = OptimizationState( master, @@ -126,6 +127,7 @@ end function optimize!( form::MathProg.Formulation, env::Env, initial_primal_bound, initial_dual_bound ) + MathProg.bounds_propagation!(form) initstate = OptimizationState( form, ip_primal_bound = initial_primal_bound, diff --git a/test/MathOptInterface/MOI_wrapper.jl b/test/MathOptInterface/MOI_wrapper.jl index 14c147ace..c99b107c5 100644 --- a/test/MathOptInterface/MOI_wrapper.jl +++ b/test/MathOptInterface/MOI_wrapper.jl @@ -5,7 +5,6 @@ MOI.set(OPTIMIZER, MOI.RawParameter("default_optimizer"), GLPK.Optimizer) const CONFIG = MOIT.TestConfig(atol=1e-6, rtol=1e-6, infeas_certificates = false) - @testset "SolverName" begin @test MOI.get(OPTIMIZER, MOI.SolverName()) == "Coluna" end @@ -111,6 +110,43 @@ end @test JuMP.objective_value(model) == 2.0 end +# @testset "Multiple single variable constraints" begin +# coluna = optimizer_with_attributes( +# Coluna.Optimizer, +# "params" => Coluna.Params( +# solver=Coluna.Algorithm.TreeSearchAlgorithm() +# ), +# "default_optimizer" => GLPK.Optimizer +# ) + +# @axis(M, 1:1) +# J = 1:1 + +# model = BlockModel(coluna) +# @variable(model, x[m in M, j in J]) +# @constraint(model, c1[m in M], 1 <= x[m,1] <= 2) +# @constraint(model, c2[m in M], x[m,1] >= 1.2) +# @constraint(model, c3[m in M], x[m,1] <= 1.8) +# @constraint(model, c4[m in M], x[m,1] <= 1.7) +# @constraint(model, c5[m in M], x[m,1] == 1.6) +# @objective(model, Max, sum(x[m,j] for m in M, j in J)) + +# @dantzig_wolfe_decomposition(model, decomposition, M) + +# optimize!(model) +# @test JuMP.objective_value(model) == 1.6 + +# delete(model, c5[M[1]]) +# unregister(model, :c5) +# optimize!(model) +# @test JuMP.objective_value(model) == 1.7 + +# delete(model, c4[M[1]]) +# unregister(model, :c4) +# optimize!(model) +# @test JuMP.objective_value(model) == 1.8 +# end + const UNSUPPORTED_TESTS = [ "solve_qcp_edge_cases", # Quadratic constraints not supported "delete_nonnegative_variables", # `VectorOfVariables`-in-`Nonnegatives` not supported @@ -131,7 +167,6 @@ const UNSUPPORTED_TESTS = [ "solve_unbounded_model", # default lower bound 0 ] -MathOptInterface.Test.getconstraint const BASIC = [ "add_variable", "solver_name", @@ -175,25 +210,11 @@ const LP_TESTS = [ "solve_affine_lessthan" ] -const CONSTRAINTDUAL_SINGLEVAR = [ - "solve_with_lowerbound", - "solve_singlevariable_obj", - "solve_constant_obj", - "solve_single_variable_dual_max", - "solve_single_variable_dual_min", - "solve_duplicate_terms_obj", - "solve_blank_obj", - "solve_with_upperbound", - "linear1", - "linear2", - "linear10b", - "linear14" -] - -const DELETE_SINGLEVAR_CONSTR = [ - # BUG: issue #583 - "linear5", - "linear14" +const CONSTRAINTDUAL_SINGLEVAR = String[ + "solve_single_variable_dual_max", # TODO bug + "solve_single_variable_dual_min", # TODO bug + "linear14", # TODO bug + "linear1", # TODO bug ] const UNCOVERED_TERMINATION_STATUS = [ @@ -201,13 +222,6 @@ const UNCOVERED_TERMINATION_STATUS = [ "linear8c" # DUAL_INFEASIBLE or INFEASIBLE_OR_UNBOUNDED required ] -const SET_CONSTRAINTSET = [ - # BUG - "linear4", - "linear6", - "linear7" -] - @testset "Unit Basic/MIP" begin MOI.set(OPTIMIZER, MOI.RawParameter("params"), CL.Params(solver = ClA.SolveIpForm())) MOIT.unittest(OPTIMIZER, CONFIG, vcat(UNSUPPORTED_TESTS, LP_TESTS, MIP_TESTS)) @@ -236,7 +250,7 @@ end @testset "Continuous Linear" begin MOIT.contlineartest(BRIDGED, CONFIG, vcat( - CONSTRAINTDUAL_SINGLEVAR, DELETE_SINGLEVAR_CONSTR, UNCOVERED_TERMINATION_STATUS, SET_CONSTRAINTSET, [ + CONSTRAINTDUAL_SINGLEVAR, UNCOVERED_TERMINATION_STATUS, [ "partial_start" # VariablePrimalStart not supported ] )) diff --git a/test/custom_var_cuts_tests.jl b/test/custom_var_cuts_tests.jl index 4fb31981d..a76d30e1b 100644 --- a/test/custom_var_cuts_tests.jl +++ b/test/custom_var_cuts_tests.jl @@ -74,7 +74,7 @@ function custom_var_cuts_test() # Get the dual values of the custom cuts custduals = Tuple{Int, Float64}[] - for (_, constr) in getconstrs(cbdata.form.parent_formulation) + for (_, constr) in Coluna.MathProg.getconstrs(cbdata.form.parent_formulation) if typeof(constr.custom_data) == MyCustomCutData push!(custduals, ( constr.custom_data.min_items, @@ -122,6 +122,7 @@ function custom_var_cuts_test() solver = my_pricing_callback ) + cut_ids = [] function custom_cut_sep(cbdata) # compute the constraint violation viol = -1.0 @@ -136,17 +137,17 @@ function custom_var_cuts_test() # add the cut (at most one variable with 2 or more of the 3 items) if violated if viol > 0.001 - MOI.submit( + cut_id = MOI.submit( model, MOI.UserCut(cbdata), JuMP.ScalarConstraint(JuMP.AffExpr(0.0), MOI.LessThan(1.0)), MyCustomCutData(2) ) + push!(cut_ids, cut_id) end return end MOI.set(model, MOI.UserCutCallback(), custom_cut_sep) JuMP.optimize!(model) - @show JuMP.objective_value(model) @test JuMP.termination_status(model) == MOI.OPTIMAL end diff --git a/test/issues_tests.jl b/test/issues_tests.jl index d67165877..f516a06cd 100644 --- a/test/issues_tests.jl +++ b/test/issues_tests.jl @@ -258,7 +258,7 @@ function continuous_vars_in_sp() function solve_flow_model(f1_integer, coluna) @axis(M, 1:1) model = BlockDecomposition.BlockModel(coluna, direct_model=true) - @variable(model, f[1:3, m in M]) + @variable(model, f[1:3, m in M] >= 0) if f1_integer JuMP.set_integer(f[1, 1]) end diff --git a/test/unit/MathProg/variables.jl b/test/unit/MathProg/variables.jl index d764ab024..4f48905d8 100644 --- a/test/unit/MathProg/variables.jl +++ b/test/unit/MathProg/variables.jl @@ -10,39 +10,41 @@ function getset_variables() kind = ClF.Integ, inc_val = 4.0 ) - @test ClF.getperencost(form, var) == 2.0 - @test ClF.getperenlb(form, var) == -1.0 - @test ClF.getperenub(form, var) == 1.0 - @test ClF.getperensense(form, var) == ClF.Free - @test ClF.getperenkind(form, var) == ClF.Integ - @test ClF.getperenincval(form, var) == 4.0 + varid = getid(var) - @test ClF.getcurcost(form, var) == 2.0 - @test ClF.getcurlb(form, var) == -1.0 - @test ClF.getcurub(form, var) == 1.0 - @test ClF.getcursense(form, var) == ClF.Free - @test ClF.getcurkind(form, var) == ClF.Integ - @test ClF.getcurincval(form, var) == 4.0 + @test ClF.getperencost(form, varid) == 2.0 + @test ClF.getperenlb(form, varid) == -1.0 + @test ClF.getperenub(form, varid) == 1.0 + @test ClF.getperensense(form, varid) == ClF.Free + @test ClF.getperenkind(form, varid) == ClF.Integ + @test ClF.getperenincval(form, varid) == 4.0 - ClF.setcurcost!(form, var, 3.0) - ClF.setcurlb!(form, var, -2.0) - ClF.setcurub!(form, var, 2.0) - ClF.setcurkind!(form, var, ClF.Continuous) - ClF.setcurincval!(form, var, 3.0) + @test ClF.getcurcost(form, varid) == 2.0 + @test ClF.getcurlb(form, varid) == -1.0 + @test ClF.getcurub(form, varid) == 1.0 + @test ClF.getcursense(form, varid) == ClF.Free + @test ClF.getcurkind(form, varid) == ClF.Integ + @test ClF.getcurincval(form, varid) == 4.0 - @test ClF.getcurcost(form, var) == 3.0 - @test ClF.getcurlb(form, var) == -2.0 - @test ClF.getcurub(form, var) == 2.0 - @test ClF.getcursense(form, var) == ClF.Free - @test ClF.getcurkind(form, var) == ClF.Continuous - @test ClF.getcurincval(form, var) == 3.0 + ClF.setcurcost!(form, varid, 3.0) + ClF.setcurlb!(form, varid, -2.0) + ClF.setcurub!(form, varid, 2.0) + ClF.setcurkind!(form, varid, ClF.Continuous) + ClF.setcurincval!(form, varid, 3.0) - ClF.reset!(form, var) - @test ClF.getcurcost(form, var) == 2.0 - @test ClF.getcurlb(form, var) == -1.0 - @test ClF.getcurub(form, var) == 1.0 - @test ClF.getcursense(form, var) == ClF.Free - @test ClF.getcurkind(form, var) == ClF.Integ - @test ClF.getcurincval(form, var) == 4.0 + @test ClF.getcurcost(form, varid) == 3.0 + @test ClF.getcurlb(form, varid) == -2.0 + @test ClF.getcurub(form, varid) == 2.0 + @test ClF.getcursense(form, varid) == ClF.Free + @test ClF.getcurkind(form, varid) == ClF.Continuous + @test ClF.getcurincval(form, varid) == 3.0 + + ClF.reset!(form, varid) + @test ClF.getcurcost(form, varid) == 2.0 + @test ClF.getcurlb(form, varid) == -1.0 + @test ClF.getcurub(form, varid) == 1.0 + @test ClF.getcursense(form, varid) == ClF.Free + @test ClF.getcurkind(form, varid) == ClF.Integ + @test ClF.getcurincval(form, varid) == 4.0 return end \ No newline at end of file diff --git a/test/unit/constraint.jl b/test/unit/constraint.jl index 405b1672f..45c73d8d5 100644 --- a/test/unit/constraint.jl +++ b/test/unit/constraint.jl @@ -14,19 +14,78 @@ function moi_constr_record_getters_and_setters_tests() end function constraint_getters_and_setters_tests() - form = create_formulation!(Env(Coluna.Params()), Original()) + # Constraint + c = ClF.setconstr!(form, "fake_constr", ClF.MasterBranchOnOrigVarConstr, - rhs = -13.0, kind = ClF.Facultative, sense = ClF.Equal, - inc_val = -12.0, is_active = false, is_explicit = false + rhs = -13.0, kind = ClF.Facultative, sense = ClF.Less, + inc_val = -12.0, is_active = false, is_explicit = false ) + + cid = getid(c) - ClF.setcurrhs!(form, c, 10.0) - @test ClF.getcurrhs(form,c) == 10.0 - @test ClF.getperenrhs(form,c) == -13.0 + # rhs + @test ClF.getcurrhs(form, cid) == -13.0 + ClF.setcurrhs!(form, cid, 10.0) + @test ClF.getperenrhs(form, cid) == -13.0 + @test ClF.getcurrhs(form, cid) == 10.0 + + ClF.setperenrhs!(form, cid, 45.0) + @test ClF.getperenrhs(form, cid) == 45.0 + @test ClF.getcurrhs(form, cid) == 45.0 + ClF.setcurrhs!(form, cid, 12.0) # change cur before reset! + + # sense + @test ClF.getcursense(form, cid) == ClF.Less + ClF.setcursense!(form, cid, ClF.Greater) + @test ClF.getperensense(form, cid) == ClF.Less + @test ClF.getcursense(form, cid) == ClF.Greater + + ClF.setperensense!(form, cid, ClF.Equal) + @test ClF.getperensense(form, cid) == ClF.Equal + @test ClF.getcursense(form, cid) == ClF.Equal - ClF.reset!(form, c) - @test ClF.getcurrhs(form, c) == -13.0 - @test ClF.getperenrhs(form, c) == -13.0 + ClF.reset!(form, cid) + @test ClF.getcurrhs(form, cid) == 45.0 + @test ClF.getperenrhs(form, cid) == 45.0 + + # Single variable constraint + + v = ClF.setvar!(form, "x", ClF.OriginalVar) + + c = ClF.setsinglevarconstr!( + form, "fake_single_var_constr", getid(v), ClF.OriginalConstr; rhs = -2.0, + kind = ClF.Essential, sense = ClF.Equal, inc_val = -12.0, is_active = true + ) + + cid = getid(c) + + # rhs + @test ClF.getcurrhs(form, cid) == -2.0 + ClF.setcurrhs!(form, cid, -10.0) + @test ClF.getcurrhs(form, cid) == -10.0 + @test ClF.getperenrhs(form, cid) == -2.0 + + ClF.setperenrhs!(form, cid, 33.0) + @test ClF.getperenrhs(form, cid) == 33.0 + @test ClF.getcurrhs(form, cid) == 33.0 + + # sense + @test ClF.getcursense(form,cid) == ClF.Equal + ClF.setcursense!(form, cid, ClF.Less) + @test ClF.getcursense(form,cid) == ClF.Less + @test ClF.getperensense(form,cid) == ClF.Equal + + ClF.setperensense!(form, cid, ClF.Greater) + @test ClF.getperensense(form, cid) == ClF.Greater + @test ClF.getcursense(form, cid) == ClF.Greater + + # explicit + @test ClF.isexplicit(form, cid) + @test ClF.isexplicit(form, c) + + # name + @test ClF.getname(form,cid) == "fake_single_var_constr" + return end diff --git a/test/unit/containers/solsandbounds.jl b/test/unit/containers/solsandbounds.jl index e00be69a4..97897b2e2 100644 --- a/test/unit/containers/solsandbounds.jl +++ b/test/unit/containers/solsandbounds.jl @@ -318,8 +318,8 @@ function solution_unit() primalsol2 = PrimalSolution(form, [getid(var)], [0.0], 0.0, CB.UNKNOWN_FEASIBILITY) @test isless(primalsol1, primalsol2) # primalsol1 is worse than primalsol2 for min sense - dualsol1 = DualSolution(form, [getid(constr)], [1.0], 1.0, CB.UNKNOWN_FEASIBILITY) - dualsol2 = DualSolution(form, [getid(constr)], [0.0], 0.0, CB.UNKNOWN_FEASIBILITY) + dualsol1 = DualSolution(form, [getid(constr)], [1.0], VarId[], Float64[], ActiveBound[], 1.0, CB.UNKNOWN_FEASIBILITY) + dualsol2 = DualSolution(form, [getid(constr)], [0.0], VarId[], Float64[], ActiveBound[], 0.0, CB.UNKNOWN_FEASIBILITY) @test isless(dualsol2, dualsol1) # dualsol2 is worse than dualsol1 for min sense # MaxSense @@ -333,8 +333,8 @@ function solution_unit() primalsol2 = PrimalSolution(form, [getid(var)], [0.0], 0.0, CB.UNKNOWN_FEASIBILITY) @test isless(primalsol2, primalsol1) # primalsol2 is worse than primalsol1 for max sense - dualsol1 = DualSolution(form, [getid(constr)], [1.0], 1.0, CB.UNKNOWN_FEASIBILITY) - dualsol2 = DualSolution(form, [getid(constr)], [0.0], 0.0, CB.UNKNOWN_FEASIBILITY) + dualsol1 = DualSolution(form, [getid(constr)], [1.0], VarId[], Float64[], ActiveBound[], 1.0, CB.UNKNOWN_FEASIBILITY) + dualsol2 = DualSolution(form, [getid(constr)], [0.0], VarId[], Float64[], ActiveBound[], 0.0, CB.UNKNOWN_FEASIBILITY) @test isless(dualsol1, dualsol2) # dualsol1 is worse than dualsol2 for max sense end return diff --git a/test/unit/optimizationstate.jl b/test/unit/optimizationstate.jl index 253a0f9d6..eecb4d53b 100644 --- a/test/unit/optimizationstate.jl +++ b/test/unit/optimizationstate.jl @@ -16,7 +16,7 @@ function update_sol_tests() max_length_lp_primal_sols = 2.0, max_length_lp_dual_sols = 2.0 ) primalsol = PrimalSolution(form, [getid(var)], [2.0], 2.0, CB.UNKNOWN_FEASIBILITY) - dualsol = DualSolution(form, [getid(constr)], [1.0], 1.0, CB.UNKNOWN_FEASIBILITY) + dualsol = DualSolution(form, [getid(constr)], [1.0], VarId[], Float64[], ActiveBound[], 1.0, CB.UNKNOWN_FEASIBILITY) ### ip primal update_ip_primal_sol!(state, primalsol) @@ -56,7 +56,7 @@ function update_sol_tests() # check that incumbent bound is updated @test get_lp_dual_bound(state) == 1.0 update_lp_dual_sol!(state, DualSolution( - form, [getid(constr)], [0.0], 0.0, CB.UNKNOWN_FEASIBILITY + form, [getid(constr)], [0.0], VarId[], Float64[], ActiveBound[], 0.0, CB.UNKNOWN_FEASIBILITY )) # check that solution worse than `dualsol` is NOT added to `state.lp_dual_sols` @test length(get_lp_dual_sols(state)) == 1 @@ -76,7 +76,7 @@ function update_sol_tests() max_length_lp_primal_sols = 2.0, max_length_lp_dual_sols = 2.0 ) primalsol = PrimalSolution(form, [getid(var)], [1.0], 1.0, CB.UNKNOWN_FEASIBILITY) - dualsol = DualSolution(form, [getid(constr)], [2.0], 2.0, CB.UNKNOWN_FEASIBILITY) + dualsol = DualSolution(form, [getid(constr)], [2.0], VarId[], Float64[], ActiveBound[], 2.0, CB.UNKNOWN_FEASIBILITY) ### ip primal update_ip_primal_sol!(state, primalsol) @@ -116,7 +116,7 @@ function update_sol_tests() # check that incumbent bound is updated @test get_lp_dual_bound(state) == 2.0 update_lp_dual_sol!(state, DualSolution( - form, [getid(constr)], [3.0], 3.0, CB.UNKNOWN_FEASIBILITY + form, [getid(constr)], [3.0], VarId[], Float64[], ActiveBound[], 3.0, CB.UNKNOWN_FEASIBILITY )) # check that solution worse than `dualsol` is NOT added to `state.lp_dual_sols` @test length(get_lp_dual_sols(state)) == 1 @@ -133,7 +133,7 @@ function add_sol_tests() constr = ClMP.setconstr!(form, "constr1", ClMP.OriginalConstr) state = OptimizationState(form) primalsol = PrimalSolution(form, [getid(var)], [2.0], 2.0, CB.UNKNOWN_FEASIBILITY) - dualsol = DualSolution(form, [getid(constr)], [1.0], 1.0, CB.UNKNOWN_FEASIBILITY) + dualsol = DualSolution(form, [getid(constr)], [1.0], VarId[], Float64[], ActiveBound[], 1.0, CB.UNKNOWN_FEASIBILITY) ### ip primal add_ip_primal_sols!( @@ -164,7 +164,7 @@ function add_sol_tests() ### lp dual add_lp_dual_sol!(state, DualSolution( - form, [getid(constr)], [0.0], 0.0, CB.UNKNOWN_FEASIBILITY + form, [getid(constr)], [0.0], VarId[], Float64[], ActiveBound[], 0.0, CB.UNKNOWN_FEASIBILITY )) # check that incumbent bound is updated @test get_lp_dual_bound(state) == 0.0 @@ -186,7 +186,7 @@ function add_sol_tests() constr = ClMP.setconstr!(form, "constr1", ClMP.OriginalConstr) state = OptimizationState(form) primalsol = PrimalSolution(form, [getid(var)], [1.0], 1.0, CB.UNKNOWN_FEASIBILITY) - dualsol = DualSolution(form, [getid(constr)], [2.0], 2.0, CB.UNKNOWN_FEASIBILITY) + dualsol = DualSolution(form, [getid(constr)], [2.0], VarId[], Float64[], ActiveBound[], 2.0, CB.UNKNOWN_FEASIBILITY) ### ip primal add_ip_primal_sols!( @@ -217,7 +217,7 @@ function add_sol_tests() ### lp dual add_lp_dual_sol!(state, DualSolution( - form, [getid(constr)], [3.0], 3.0, CB.UNKNOWN_FEASIBILITY + form, [getid(constr)], [3.0], VarId[], Float64[], ActiveBound[], 3.0, CB.UNKNOWN_FEASIBILITY )) # check that incumbent bound is updated @test get_lp_dual_bound(state) == 3.0 @@ -241,7 +241,7 @@ function set_sol_tests() form, ip_primal_bound = 3.0, lp_primal_bound = 3.0, lp_dual_bound = -1.0 ) primalsol = PrimalSolution(form, [getid(var)], [2.0], 2.0, CB.UNKNOWN_FEASIBILITY) - dualsol = DualSolution(form, [getid(constr)], [0.0], 0.0, CB.UNKNOWN_FEASIBILITY) + dualsol = DualSolution(form, [getid(constr)], [0.0], VarId[], Float64[], ActiveBound[], 0.0, CB.UNKNOWN_FEASIBILITY) ### ip primal set_ip_primal_sol!(state, PrimalSolution( @@ -269,7 +269,7 @@ function set_sol_tests() ### lp dual set_lp_dual_sol!(state, DualSolution( - form, [getid(constr)], [1.0], 1.0, CB.UNKNOWN_FEASIBILITY + form, [getid(constr)], [1.0], VarId[], Float64[], ActiveBound[], 1.0, CB.UNKNOWN_FEASIBILITY )) set_lp_dual_sol!(state, dualsol) # check that only the solution which was set last is in `state.lp_dual_sols` @@ -291,7 +291,7 @@ function set_sol_tests() form, ip_primal_bound = -1.0, lp_primal_bound = -1.0, lp_dual_bound = 3.0 ) primalsol = PrimalSolution(form, [getid(var)], [0.0], 0.0, CB.UNKNOWN_FEASIBILITY) - dualsol = DualSolution(form, [getid(constr)], [2.0], 2.0, CB.UNKNOWN_FEASIBILITY) + dualsol = DualSolution(form, [getid(constr)], [2.0], VarId[], Float64[], ActiveBound[], 2.0, CB.UNKNOWN_FEASIBILITY) ### ip primal set_ip_primal_sol!(state, PrimalSolution( @@ -319,7 +319,7 @@ function set_sol_tests() ### lp dual set_lp_dual_sol!(state, DualSolution( - form, [getid(constr)], [1.0], 1.0, CB.UNKNOWN_FEASIBILITY + form, [getid(constr)], [1.0], VarId[], Float64[], ActiveBound[], 1.0, CB.UNKNOWN_FEASIBILITY )) set_lp_dual_sol!(state, dualsol) # check that only the solution which was set last is in `state.lp_dual_sols` diff --git a/test/unit/variable.jl b/test/unit/variable.jl index 3673fed37..046cf1146 100644 --- a/test/unit/variable.jl +++ b/test/unit/variable.jl @@ -33,7 +33,7 @@ function variable_getters_and_setters_tests() ) v = ClF.Variable( - ClF.Id{ClF.Variable}(ClF.MasterPureVar, 23, 10), "fake_var"; + ClF.VarId(ClF.MasterPureVar, 23, 10, false), "fake_var"; var_data = v_data )