From 99e0c7711cbe07142a76097535acbcb9c882eb96 Mon Sep 17 00:00:00 2001 From: Guillaume Marques Date: Mon, 29 May 2023 20:33:40 +0200 Subject: [PATCH] Pool struct for primal solutions of colgen --- src/MOIwrapper.jl | 2 +- src/MathProg/MathProg.jl | 1 + src/MathProg/duties.jl | 15 +----- src/MathProg/formulation.jl | 55 ++-------------------- src/MathProg/pool.jl | 81 ++++++++++++++++++++++++++++++++ src/MathProg/projection.jl | 2 +- test/unit/MathProg/projection.jl | 11 ++--- 7 files changed, 95 insertions(+), 72 deletions(-) create mode 100644 src/MathProg/pool.jl diff --git a/src/MOIwrapper.jl b/src/MOIwrapper.jl index e6c786afb..bd68e9c51 100644 --- a/src/MOIwrapper.jl +++ b/src/MOIwrapper.jl @@ -1104,7 +1104,7 @@ function BD.value(info::ColumnInfo, index::MOI.VariableIndex) varid = info.optimizer.env.varids[index] origin_form_uid = getoriginformuid(info.column_var_id) spform = get_dw_pricing_sps(info.optimizer.inner.re_formulation)[origin_form_uid] - return get_primal_sol_pool(spform)[info.column_var_id,varid] + return get_primal_sol_pool(spform).solutions[info.column_var_id,varid] end function MOI.get(model::Optimizer, ::MOI.NumberOfVariables) diff --git a/src/MathProg/MathProg.jl b/src/MathProg/MathProg.jl index 58f403b2f..ea61534c4 100644 --- a/src/MathProg/MathProg.jl +++ b/src/MathProg/MathProg.jl @@ -26,6 +26,7 @@ include("bounds.jl") include("solutions.jl") include("buffer.jl") include("manager.jl") +include("pool.jl") include("duties.jl") include("formulation.jl") include("varconstr.jl") diff --git a/src/MathProg/duties.jl b/src/MathProg/duties.jl index 81ea0a3ae..4b4795d90 100644 --- a/src/MathProg/duties.jl +++ b/src/MathProg/duties.jl @@ -24,15 +24,7 @@ mutable struct DwSp <: AbstractSpDuty column_var_kind::VarKind # Pool of solutions to the Dantzig-Wolfe subproblem. - ## Coluna representation of solutions (filtered by `_sol_repr_for_pool`). - ## [colid, varid] = value - primalsols_pool::VarVarMatrix - # Hash table to quickly find identical solutions - hashtable_primalsols_pool::HashTable{VarId,VarId} - ## Perennial cost of solutions - costs_primalsols_pool::Dict{VarId, Float64} - ## Custom representation of solutions - custom_primalsols_pool::Dict{VarId, BD.AbstractCustomData} + pool::Pool end "A pricing subproblem of a formulation decomposed using Dantzig-Wolfe." @@ -40,10 +32,7 @@ function DwSp(setup_var, lower_multiplicity_constr_id, upper_multiplicity_constr return DwSp( setup_var, lower_multiplicity_constr_id, upper_multiplicity_constr_id, column_var_kind, - dynamicsparse(VarId, VarId, Float64; fill_mode = false), - HashTable{VarId, VarId}(), - Dict{VarId, Float64}(), - Dict{VarId, BD.AbstractCustomData}() + Pool() ) end mutable struct BendersSp <: AbstractSpDuty diff --git a/src/MathProg/formulation.jl b/src/MathProg/formulation.jl index 7dab73945..e77901a09 100644 --- a/src/MathProg/formulation.jl +++ b/src/MathProg/formulation.jl @@ -177,7 +177,7 @@ function _setrobustmembers!(form::Formulation, constr::Constraint, members::VarM if getduty(varid) <= MasterRepPricingVar || getduty(varid) <= MasterRepPricingSetupVar # then for all columns having its own variables for (_, spform) in get_dw_pricing_sps(form.parent_formulation) - for (col_id, col_coeff) in @view get_primal_sol_pool(spform)[:,varid] + for (col_id, col_coeff) in @view get_primal_sol_pool(spform).solutions[:,varid] coef_matrix[constrid, col_id] += col_coeff * var_coeff end end @@ -484,41 +484,7 @@ end ############################################################################################ ############################################################################################ -get_primal_sol_pool(form::Formulation{DwSp}) = form.duty_data.primalsols_pool -_get_primal_sol_pool_hash_table(form::Formulation{DwSp}) = form.duty_data.hashtable_primalsols_pool - -############################################################################################ -# Pool of solutions -# We consider that the pool is a dynamic sparse matrix. -############################################################################################ - -# Returns nothing if there is no identical solutions in pool; the id of the -# identical solution otherwise. -function _get_same_sol_in_pool(pool_sols, pool_hashtable, sol) - sols_with_same_members = getsolids(pool_hashtable, sol) - for existing_sol_id in sols_with_same_members - existing_sol = @view pool_sols[existing_sol_id,:] - if existing_sol == sol - return existing_sol_id - end - end - return nothing -end - -# We only keep variables that have certain duty in the representation of the -# solution stored in the pool. The second argument allows us to dispatch because -# filter may change depending on the duty of the formulation. -function _sol_repr_for_pool(primal_sol::PrimalSolution, ::DwSp) - var_ids = VarId[] - vals = Float64[] - for (var_id, val) in primal_sol - if getduty(var_id) <= DwSpSetupVar || getduty(var_id) <= DwSpPricingVar - push!(var_ids, var_id) - push!(vals, val) - end - end - return var_ids, vals -end +get_primal_sol_pool(form::Formulation{DwSp}) = form.duty_data.pool function initialize_solution_pool!(form::Formulation{DwSp}, initial_columns_callback::Function) master = getmaster(form) @@ -556,8 +522,7 @@ subproblem; `nothing` otherwise. function get_column_from_pool(primal_sol::PrimalSolution{Formulation{DwSp}}) spform = primal_sol.solution.model pool = get_primal_sol_pool(spform) - pool_hashtable = _get_primal_sol_pool_hash_table(spform) - return _get_same_sol_in_pool(pool, pool_hashtable, primal_sol) + return get_from_pool(pool, primal_sol) end """ @@ -588,11 +553,6 @@ function insert_column!( primal_sol ) - pool = get_primal_sol_pool(spform) - pool_hashtable = _get_primal_sol_pool_hash_table(spform) - costs_pool = spform.duty_data.costs_primalsols_pool - custom_pool = spform.duty_data.custom_primalsols_pool - # Compute coefficient members of the column in the matrix. members = _col_members(primal_sol, getcoefmatrix(master_form)) @@ -616,14 +576,9 @@ function insert_column!( # Store the solution in the pool if asked. if store_in_sp_pool + pool = get_primal_sol_pool(spform) col_id = VarId(getid(col); duty = DwSpPrimalSol) - var_ids, vals = _sol_repr_for_pool(primal_sol, spform.duty_data) - addrow!(pool, col_id, var_ids, vals) - costs_pool[col_id] = new_col_peren_cost - if primal_sol.custom_data !== nothing - custom_pool[col_id] = primal_sol.custom_data - end - savesolid!(pool_hashtable, col_id, primal_sol) + push_in_pool!(pool, primal_sol, col_id, new_col_peren_cost) end return getid(col) end diff --git a/src/MathProg/pool.jl b/src/MathProg/pool.jl new file mode 100644 index 000000000..805767228 --- /dev/null +++ b/src/MathProg/pool.jl @@ -0,0 +1,81 @@ +struct Pool + solutions::DynamicSparseArrays.DynamicSparseMatrix{VarId,VarId,Float64} + solutions_hash::ColunaBase.HashTable{VarId,VarId} + costs::Dict{VarId,Float64} + custom_data::Dict{VarId,BD.AbstractCustomData} +end + +function Pool() + return Pool( + DynamicSparseArrays.dynamicsparse(VarId, VarId, Float64; fill_mode = false), + ColunaBase.HashTable{VarId, VarId}(), + Dict{VarId, Float64}(), + Dict{VarId, BD.AbstractCustomData}() + ) +end + +# Returns nothing if there is no identical solutions in pool; the id of the +# identical solution otherwise. +function _get_same_sol_in_pool(solutions, hashtable, sol) + sols_with_same_members = ColunaBase.getsolids(hashtable, sol) + for existing_sol_id in sols_with_same_members + existing_sol = @view solutions[existing_sol_id,:] + if existing_sol == sol + return existing_sol_id + end + end + return nothing +end + +# We only keep variables that have certain duty in the representation of the +# solution stored in the pool. The second argument allows us to dispatch because +# filter may change depending on the duty of the formulation. +function _sol_repr_for_pool(primal_sol::PrimalSolution) + var_ids = VarId[] + vals = Float64[] + for (var_id, val) in primal_sol + if getduty(var_id) <= DwSpSetupVar || getduty(var_id) <= DwSpPricingVar + push!(var_ids, var_id) + push!(vals, val) + end + end + return var_ids, vals +end + +""" + same_custom_data(custom_data1, custom_data2) -> Bool + +Returns `true`if the custom data are the same, false otherwise. +""" +same_custom_data(custom_data1, custom_data2) = custom_data1 == custom_data2 + +function get_from_pool(pool::Pool, solution) + existing_sol_id = _get_same_sol_in_pool(pool.solutions, pool.solutions_hash, solution) + if isnothing(existing_sol_id) + return nothing + end + + # When there are non-robust cuts, Coluna has not enough information to identify that two + # columns are identical. The columns may be mapped into the same original variables but + # be internally different, meaning that the coefficients of non-robust cuts to be added + # in the future may differ. This is why we need to check custom data. + custom_data1 = get(pool.custom_data, existing_sol_id, nothing) + custom_data2 = solution.custom_data + if same_custom_data(custom_data1, custom_data2) + return existing_sol_id + end + return nothing +end + +function push_in_pool!(pool::Pool, solution, sol_id, cost) + var_ids, vals = _sol_repr_for_pool(solution) + DynamicSparseArrays.addrow!(pool.solutions, sol_id, var_ids, vals) + pool.costs[sol_id] = cost + if !isnothing(solution.custom_data) + pool.custom_data[sol_id] = solution.custom_data + end + ColunaBase.savesolid!(pool.solutions_hash, sol_id, solution) + return true +end + + diff --git a/src/MathProg/projection.jl b/src/MathProg/projection.jl index 96b85af23..e03cb9ec2 100644 --- a/src/MathProg/projection.jl +++ b/src/MathProg/projection.jl @@ -89,7 +89,7 @@ function _extract_data_for_mapping(sol::PrimalSolution{Formulation{DwMaster}}) if duty <= MasterCol origin_form_uid = getoriginformuid(varid) spform = get_dw_pricing_sps(master.parent_formulation)[origin_form_uid] - column = @view get_primal_sol_pool(spform)[varid,:] + column = @view get_primal_sol_pool(spform).solutions[varid,:] if !haskey(columns, origin_form_uid) columns[origin_form_uid] = DynamicMatrixColView{VarId, VarId, Float64}[] values[origin_form_uid] = Float64[] diff --git a/test/unit/MathProg/projection.jl b/test/unit/MathProg/projection.jl index 209f446b6..75c721168 100644 --- a/test/unit/MathProg/projection.jl +++ b/test/unit/MathProg/projection.jl @@ -147,11 +147,10 @@ function projection_from_dw_reform_to_master_1() # Register column in the pool spform = first(sps) pool = ClMP.get_primal_sol_pool(spform) - pool_hashtable = ClMP._get_primal_sol_pool_hash_table(spform) - costs_pool = spform.duty_data.costs_primalsols_pool - custom_pool = spform.duty_data.custom_primalsols_pool - var_ids = map(n -> mastervarids[n], ["x_12", "x_13", "x_14", "x_23", "x_24", "x_34"]) + var_ids = map(n -> ClMP.getid(ClMP.getvar(spform, mastervarids[n])), ["x_12", "x_13", "x_14", "x_23", "x_24", "x_34"]) + + @show spform # VarId[Variableu2, Variableu1, Variableu3, Variableu4, Variableu5, Variableu6] for (name, vals) in Iterators.zip( ["MC1", "MC2", "MC3", "MC4"], @@ -163,9 +162,7 @@ function projection_from_dw_reform_to_master_1() ] ) col_id = ClMP.VarId(mastervarids[name]; duty = DwSpPrimalSol) - addrow!(pool, col_id, var_ids, vals) - costs_pool[col_id] = 1.0 - ClMP.savesolid!(pool_hashtable, col_id, ClMP.PrimalSolution(spform, var_ids, vals, 1.0, ClMP.FEASIBLE_SOL)) + ClMP.push_in_pool!(pool, ClMP.PrimalSolution(spform, var_ids, vals, 1.0, ClMP.FEASIBLE_SOL), col_id, 1.0) end # Create primal solution where each route is used 1/2 time.