From 2df56cf2a8ae9098c9139b5f18a32f4948ebcb56 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 2 Mar 2022 15:42:26 +1300 Subject: [PATCH 1/8] WIP: update to MatrixOfConstraints --- src/MOI_wrapper/MOI_wrapper.jl | 704 +++++++++++---------------------- 1 file changed, 226 insertions(+), 478 deletions(-) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index 57fffdd..d5ef88b 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -1,5 +1,55 @@ const MOI = MathOptInterface +MOI.Utilities.@product_of_sets( + _LPProductOfSets, + MOI.EqualTo{T}, + MOI.LessThan{T}, + MOI.GreaterThan{T}, + MOI.Interval{T}, +) + +MOI.Utilities.@struct_of_constraints_by_set_types( + _CbcConstraints, + Union{ + MOI.EqualTo{T}, + MOI.LessThan{T}, + MOI.GreaterThan{T}, + MOI.Interval{T}, + }, + MOI.ZeroOne, + MOI.Integer, + MOI.SOS1{T}, + MOI.SOS2{T}, +) + +const OptimizerCache = MOI.Utilities.GenericModel{ + Float64, + MOI.Utilities.ObjectiveContainer{Float64}, + MOI.Utilities.VariablesContainer{Float64}, + _CbcConstraints{Float64}{ + MOI.Utilities.MatrixOfConstraints{ + Float64, + MOI.Utilities.MutableSparseMatrixCSC{ + Float64, + Cint, + MOI.Utilities.ZeroBasedIndexing, + }, + MOI.Utilities.Hyperrectangle{Float64}, + _LPProductOfSets{Float64}, + }, + MOI.Utilities.VectorOfConstraints{MOI.VariableIndex,MOI.ZeroOne}, + MOI.Utilities.VectorOfConstraints{MOI.VariableIndex,MOI.Integer}, + MOI.Utilities.VectorOfConstraints{ + MOI.VectorOfVariables, + MOI.SOS1{Float64}, + }, + MOI.Utilities.VectorOfConstraints{ + MOI.VectorOfVariables, + MOI.SOS2{Float64}, + }, + }, +} + """ Optimizer() @@ -38,6 +88,10 @@ end Base.cconvert(::Type{Ptr{Cvoid}}, model::Optimizer) = model Base.unsafe_convert(::Type{Ptr{Cvoid}}, model::Optimizer) = model.inner +function MOI.default_cache(::Optimizer, ::Type{Float64}) + return MOI.Utilities.UniversalFallback(OptimizerCache()) +end + function MOI.supports(::Optimizer, ::MOI.RawOptimizerAttribute) # TODO(odow): There is no programatical way throught the C API to check if a # parameter name (or value) is valid. Fix this upstream. @@ -126,337 +180,229 @@ function MOI.is_empty(model::Optimizer) return Cbc_getNumCols(model) == 0 && Cbc_getNumRows(model) == 0 end -mutable struct _CbcModelFormat - num_rows::Cint - num_cols::Cint - # (row_idx, col_idx, values) are the sparse elements of the constraint matrix. - row_idx::Vector{Cint} - col_idx::Vector{Cint} - values::Vector{Float64} - # Constraint bounds. - row_lb::Vector{Float64} - row_ub::Vector{Float64} - # Variable bounds. - col_lb::Vector{Float64} - col_ub::Vector{Float64} - # Columns that are binary or integer - binary::Vector{Int} - integer::Vector{Int} - # SOS1 constraints - sos1_starts::Vector{Cint} - sos1_indices::Vector{Cint} - sos1_weights::Vector{Float64} - # SOS2 constraints - sos2_starts::Vector{Cint} - sos2_indices::Vector{Cint} - sos2_weights::Vector{Float64} - # Objective coefficients. - obj::Vector{Float64} - objective_constant::Float64 - # An `InexactError` might occur if `num_rows` or `num_cols` is too - # large, e.g., if `num_cols isa Int64` and is larger than 2^31 on - # 32-bit hardware. - function _CbcModelFormat(num_rows::Cint, num_cols::Cint) - return new( - num_rows, - num_cols, - # Constraint matrix. - Cint[], - Cint[], - Float64[], - # Row lower/upper bounds. - fill(-Inf, num_rows), - fill(Inf, num_rows), - # Column lower/upper bounds. - fill(-Inf, num_cols), - fill(Inf, num_cols), - # Binary/Integer columns. - Cint[], - Cint[], - # SOSI constraints. - Cint[], - Cint[], - Float64[], - # SOSII constraints. - Cint[], - Cint[], - Float64[], - # Objective vector and offset. - fill(0.0, num_cols), - 0.0, - ) - end +function _index_map( + src::OptimizerCache, + ::MOI.IndexMap, + ci::MOI.ConstraintIndex{<:MOI.ScalarAffineFunction}, +) + return MOI.Utilities.rows(src.constraints.moi_equalto, ci) end -### -### VariableIndex-in-{EqualTo,LessThan,GreaterThan,Interval,ZeroOne,Integer} -### - -_column_value(map::MOI.IndexMap, index::MOI.VariableIndex) = map[index].value - -function MOI.supports_constraint( - ::Optimizer, - ::Type{MOI.VariableIndex}, - ::Type{ - <:Union{ - MOI.EqualTo{Float64}, - MOI.LessThan{Float64}, - MOI.GreaterThan{Float64}, - MOI.Interval{Float64}, - MOI.ZeroOne, - MOI.Integer, - }, - }, +function _index_map( + ::OptimizerCache, + index_map::MOI.IndexMap, + ci::MOI.ConstraintIndex{<:MOI.VariableIndex}, ) - return true + return index_map[MOI.VariableIndex(ci.value)].value end -function MOI.supports_constraint( - ::Optimizer, - ::Type{MOI.VectorOfVariables}, - ::Type{<:Union{MOI.SOS1{Float64},MOI.SOS2{Float64}}}, +function _index_map( + ::OptimizerCache, + ::MOI.IndexMap, + ci::MOI.ConstraintIndex{<:MOI.VectorOfVariables}, ) - return true + return ci.value end -function _load_constraint( - ::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.VariableIndex, - set::MOI.EqualTo, -) - column = _column_value(mapping, func) - model.col_lb[column] = set.value - model.col_ub[column] = set.value +function _index_map( + src::OptimizerCache, + index_map::MOI.IndexMap, + ::Type{F}, + ::Type{S}, +) where {F,S} + inner = index_map.con_map[F, S] + for ci in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) + inner[ci] = MOI.ConstraintIndex{F,S}(_index_map(src, index_map, ci)) + end return end -function _load_constraint( - ::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.VariableIndex, - set::MOI.LessThan, -) - column = _column_value(mapping, func) - model.col_ub[column] = set.upper - return +""" + _index_map(src::OptimizerCache) + +Create an `IndexMap` mapping the variables and constraints in `OptimizerCache` +to their corresponding 1-based columns and rows. +""" +function _index_map(src::OptimizerCache) + index_map = MOI.IndexMap() + for (i, x) in enumerate(MOI.get(src, MOI.ListOfVariableIndices())) + index_map[x] = MOI.VariableIndex(i) + end + for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) + _index_map(src, index_map, F, S) + end + return index_map end -function _load_constraint( - ::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.VariableIndex, - set::MOI.GreaterThan, -) - column = _column_value(mapping, func) - model.col_lb[column] = set.lower - return +function _constraint_matrix(constraints, n) + @assert n == constraints.coefficients.n + return ( + m = constraints.coefficients.m, + n = constraints.coefficients.n, + colptr = constraints.coefficients.colptr, + rowval = constraints.coefficients.rowval, + nzval = constraints.coefficients.nzval, + lower = constraints.constants.lower, + upper = constraints.constants.upper, + ) end -function _load_constraint( - ::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.VariableIndex, - set::MOI.Interval, -) - column = _column_value(mapping, func) - model.col_lb[column] = set.lower - model.col_ub[column] = set.upper - return +function _constraint_matrix(::Nothing, n) + return ( + m = 0, + n = n, + colptr = Cint[], + rowval = Cint[], + nzval = Cdouble[], + lower = Cdouble[], + upper = Cdouble[], + ) end -function _load_constraint( - ::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.VariableIndex, - ::MOI.ZeroOne, -) - push!(model.binary, _column_value(mapping, func) - 1) - return +function MOI.copy_to(dest::Optimizer, src::OptimizerCache) + matrix = _constraint_matrix( + src.constraints.moi_equalto, + MOI.get(src, MOI.NumberOfVariables()), + ) + c = zeros(matrix.n) + obj = + MOI.get(src, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) + for term in obj.terms + c[term.variable.value] += term.coefficient + end + dest.objective_constant = obj.constant + zeroone_attr = MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}() + binaries = Cint[Cint(ci.value - 1) for ci in MOI.get(src, zeroone_attr)] + variable_lower = copy(src.variables.lower) + variable_upper = copy(src.variables.upper) + for b in binaries + variable_lower[b+1] = max(variable_lower[b+1], 0.0) + variable_upper[b+1] = min(variable_upper[b+1], 1.0) + end + Cbc_loadProblem( + dest, + matrix.n, + matrix.m, + matrix.colptr, + matrix.rowval, + matrix.nzval, + variable_lower, + variable_upper, + c, + matrix.lower, + matrix.upper, + ) + sense = MOI.get(src, MOI.ObjectiveSense()) + if sense == MOI.MIN_SENSE + Cbc_setObjSense(dest, 1) + elseif sense == MOI.MAX_SENSE + Cbc_setObjSense(dest, -1) + else + @assert sense == MOI.FEASIBILITY_SENSE + Cbc_setObjSense(dest, 0) + end + Cbc_setInteger.(dest, binaries) + attr = MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}() + for ci in MOI.get(src, attr) + Cbc_setInteger(dest, Cint(ci.value - 1)) + end + any_sos = false + for (S, type) in ((MOI.SOS1{Float64}, 1), (MOI.SOS2{Float64}, 2)) + starts, indices, weights = Cint[], Cint[], Float64[] + attr = MOI.ListOfConstraintIndices{MOI.VectorOfVariables,S}() + for ci in MOI.get(src, attr) + any_sos = true + push!(starts, length(weights)) + f = MOI.get(src, MOI.ConstraintFunction(), ci) + for x in f.variables + push!(indices, Cint(x.value - 1)) + end + s = MOI.get(src, MOI.ConstraintSet(), ci) + append!(weights, s.weights) + end + N = Cint(length(starts)) + if N > 0 + Cbc_addSOS(dest, N, starts, indices, weights, Cint(type)) + end + end + if any_sos && Cbc_getNumIntegers(dest) == 0 + @warn( + "There are known correctness issues using Cbc with SOS " * + "constraints and no binary variables.", + ) + end + return _index_map(src) end -function _load_constraint( - ::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.VariableIndex, - ::MOI.Integer, +function MOI.copy_to( + dest::Optimizer, + src::MOI.Utilities.UniversalFallback{OptimizerCache}, ) - push!(model.integer, _column_value(mapping, func) - 1) - return + MOI.Utilities.throw_unsupported(src) + return MOI.copy_to(dest, src.model) end -function _load_constraints( - model::_CbcModelFormat, - src::MOI.ModelLike, - mapping::MOI.IndexMap, - F::Type{<:MOI.AbstractFunction}, - S::Type{<:MOI.AbstractSet}, -) - for index in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) - _load_constraint( - index, - model, - mapping, - MOI.get(src, MOI.ConstraintFunction(), index), - MOI.get(src, MOI.ConstraintSet(), index), - ) +function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) + cache = MOI.default_cache(dest, Float64) + src_cache = MOI.copy_to(cache, src) + cache_dest = MOI.copy_to(dest, cache) + index_map = MOI.IndexMap() + for (src_x, cache_x) in src_cache.var_map + index_map[src_x] = cache_dest[cache_x] end - return + for (src_ci, cache_ci) in src_cache.con_map + index_map[src_ci] = cache_dest[cache_ci] + end + return index_map end ### -### ScalarAffineFunction-in-{EqualTo, LessThan, GreaterThan, Interval} +### supports and supports_constraint ### function MOI.supports_constraint( ::Optimizer, - ::Type{MOI.ScalarAffineFunction{Float64}}, + ::Type{MOI.VariableIndex}, ::Type{ <:Union{ MOI.EqualTo{Float64}, MOI.LessThan{Float64}, MOI.GreaterThan{Float64}, MOI.Interval{Float64}, + MOI.ZeroOne, + MOI.Integer, }, }, ) return true end -function _add_terms( - model::_CbcModelFormat, - mapping::MOI.IndexMap, - index::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}, - func::MOI.ScalarAffineFunction{Float64}, -) where {S} - for term in func.terms - push!(model.row_idx, mapping[index].value) - push!(model.col_idx, _column_value(mapping, term.variable)) - push!(model.values, term.coefficient) - end - return -end - -function _load_constraint( - index::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.ScalarAffineFunction, - set::MOI.EqualTo, -) - _add_terms(model, mapping, index, func) - row = mapping[index].value - model.row_lb[row] = set.value - func.constant - model.row_ub[row] = set.value - func.constant - return -end - -function _load_constraint( - index::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.ScalarAffineFunction, - set::MOI.GreaterThan, -) - _add_terms(model, mapping, index, func) - row = mapping[index].value - model.row_lb[row] = set.lower - func.constant - return -end - -function _load_constraint( - index::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.ScalarAffineFunction, - set::MOI.LessThan, -) - _add_terms(model, mapping, index, func) - row = mapping[index].value - model.row_ub[row] = set.upper - func.constant - return -end - -function _load_constraint( - index::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.ScalarAffineFunction, - set::MOI.Interval, -) - _add_terms(model, mapping, index, func) - row = mapping[index].value - model.row_ub[row] = set.upper - func.constant - model.row_lb[row] = set.lower - func.constant - return -end - -function _load_constraint( - ::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.VectorOfVariables, - set::MOI.SOS1{Float64}, +function MOI.supports_constraint( + ::Optimizer, + ::Type{MOI.VectorOfVariables}, + ::Type{<:Union{MOI.SOS1{Float64},MOI.SOS2{Float64}}}, ) - push!(model.sos1_starts, Cint(length(model.sos1_weights))) - append!(model.sos1_weights, set.weights) - for v in func.variables - push!(model.sos1_indices, _column_value(mapping, v) - Cint(1)) - end - return + return true end -function _load_constraint( - ::MOI.ConstraintIndex, - model::_CbcModelFormat, - mapping::MOI.IndexMap, - func::MOI.VectorOfVariables, - set::MOI.SOS2{Float64}, +function MOI.supports_constraint( + ::Optimizer, + ::Type{MOI.ScalarAffineFunction{Float64}}, + ::Type{ + <:Union{ + MOI.EqualTo{Float64}, + MOI.LessThan{Float64}, + MOI.GreaterThan{Float64}, + MOI.Interval{Float64}, + }, + }, ) - push!(model.sos2_starts, Cint(length(model.sos2_weights))) - append!(model.sos2_weights, set.weights) - for v in func.variables - push!(model.sos2_indices, _column_value(mapping, v) - Cint(1)) - end - return + return true end -### -### ObjectiveSense -### - MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true -function MOI.set( - model::Optimizer, - ::MOI.ObjectiveSense, - sense::MOI.OptimizationSense, -) - if sense == MOI.MAX_SENSE - Cbc_setObjSense(model, -1.0) - elseif sense == MOI.MIN_SENSE - Cbc_setObjSense(model, 1.0) - else - @assert sense == MOI.FEASIBILITY_SENSE - for col in Cint(0):Cint(Cbc_getNumCols(model) - 1) - Cbc_setObjCoeff(model, col, 0.0) - end - Cbc_setObjSense(model, 0.0) - end - return -end - -### -### ObjectiveFunction{ScalarAffineFunction} -### - function MOI.supports( ::Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, @@ -464,29 +410,6 @@ function MOI.supports( return true end -function _load_objective( - model::_CbcModelFormat, - mapping::MOI.IndexMap, - dest::Optimizer, - src::MOI.ModelLike, -) - F = MOI.get(src, MOI.ObjectiveFunctionType()) - if !MOI.supports(dest, MOI.ObjectiveFunction{F}()) - error("Objective function type $(F) not supported.") - end - f = MOI.get(src, MOI.ObjectiveFunction{F}()) - # We need to increment values of objective function with += to handle - # cases like $x_1 + x_2 + x_1$. This is safe because objective function - # is initialized with zeros in the constructor and `_load_objective` only - # gets called once. - for term in f.terms - column = _column_value(mapping, term.variable) - model.obj[column] += term.coefficient - end - model.objective_constant = f.constant - return -end - ### ### Variable starting values ### @@ -527,181 +450,6 @@ function MOI.get( return get(model.variable_start, x, nothing) end -### -### This main copy_to function. -### - -function _create_constraint_indices_for_types( - src::MOI.ModelLike, - mapping::MOI.IndexMap, - ::Type{MOI.VariableIndex}, - S::Type{<:MOI.AbstractSet}, - num_rows::Int, -) - indices = MOI.get(src, MOI.ListOfConstraintIndices{MOI.VariableIndex,S}()) - for index in indices - f = MOI.get(src, MOI.ConstraintFunction(), index) - i = mapping[f].value - mapping[index] = MOI.ConstraintIndex{MOI.VariableIndex,S}(i) - end - return num_rows -end - -function _create_constraint_indices_for_types( - src::MOI.ModelLike, - mapping::MOI.IndexMap, - F::Type{MOI.VectorOfVariables}, - S::Type{<:Union{MOI.SOS1{Float64},MOI.SOS2{Float64}}}, - num_rows::Int, -) - n = 0 - for index in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) - n += 1 - mapping[index] = MOI.ConstraintIndex{F,S}(n) - end - return num_rows -end - -function _create_constraint_indices_for_types( - src::MOI.ModelLike, - mapping::MOI.IndexMap, - F::Type{MOI.ScalarAffineFunction{Float64}}, - S::Type{<:MOI.AbstractScalarSet}, - num_rows::Int, -) - for index in MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) - num_rows += 1 - mapping[index] = MOI.ConstraintIndex{F,S}(num_rows) - end - return num_rows -end - -function _create_constraint_indices( - dest::Optimizer, - src::MOI.ModelLike, - mapping::MOI.IndexMap, -) - n = 0 - for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) - if !(MOI.supports_constraint(dest, F, S)) - throw( - MOI.UnsupportedConstraint{F,S}( - "Cbc does not support constraints of type $F-in-$S.", - ), - ) - end - # The type of `F` and `S` is not type-stable, so we use a function - # barrier (`_create_constraint_indices_for_types`) to improve - # performance. - n = _create_constraint_indices_for_types(src, mapping, F, S, n) - end - return Cint(n) -end - -function _create_variable_indices(src::MOI.ModelLike, mapping::MOI.IndexMap) - for (i, x) in enumerate(MOI.get(src, MOI.ListOfVariableIndices())) - mapping[x] = MOI.VariableIndex(i) - end - return Cint(length(mapping.var_map)) -end - -function MOI.copy_to(cbc_dest::Optimizer, src::MOI.ModelLike) - @assert MOI.is_empty(cbc_dest) - mapping = MOI.IndexMap() - num_cols = _create_variable_indices(src, mapping) - num_rows = _create_constraint_indices(cbc_dest, src, mapping) - tmp_model = _CbcModelFormat(num_rows, num_cols) - _load_objective(tmp_model, mapping, cbc_dest, src) - for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) - # The type of `F` and `S` is not type-stable, so we use a function - # barrier (`_load_constraints`) to improve performance. - _load_constraints(tmp_model, src, mapping, F, S) - end - # Since Cbc doesn't have an explicit binary variable, we need to add [0, 1] - # bounds and make it integer (which is done at the end of this function). - for column in tmp_model.binary - tmp_model.col_lb[column+1] = max(tmp_model.col_lb[column+1], 0.0) - tmp_model.col_ub[column+1] = min(tmp_model.col_ub[column+1], 1.0) - end - A = SparseArrays.sparse( - tmp_model.row_idx, - tmp_model.col_idx, - tmp_model.values, - tmp_model.num_rows, - tmp_model.num_cols, - ) - Cbc_loadProblem( - cbc_dest, - tmp_model.num_cols, - tmp_model.num_rows, - A.colptr .- Cint(1), - A.rowval .- Cint(1), - A.nzval, - tmp_model.col_lb, - tmp_model.col_ub, - tmp_model.obj, - tmp_model.row_lb, - tmp_model.row_ub, - ) - MOI.Utilities.pass_attributes( - cbc_dest, - src, - mapping, - MOI.get(src, MOI.ListOfVariableIndices()), - ) - for attr in MOI.get(src, MOI.ListOfModelAttributesSet()) - if attr isa MOI.ObjectiveFunction - continue # Already copied - end - value = MOI.get(src, attr) - if value !== nothing - mapped_value = MOI.Utilities.map_indices(mapping, value) - MOI.set(cbc_dest, attr, mapped_value) - end - end - for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) - cis_src = MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) - MOI.Utilities.pass_attributes(cbc_dest, src, mapping, cis_src) - end - cbc_dest.objective_constant = tmp_model.objective_constant - if length(tmp_model.integer) > 0 - Cbc_setInteger.(cbc_dest, tmp_model.integer) - end - if length(tmp_model.binary) > 0 - Cbc_setInteger.(cbc_dest, tmp_model.binary) - end - if length(tmp_model.sos1_starts) > 0 - push!(tmp_model.sos1_starts, Cint(length(tmp_model.sos1_weights))) - Cbc_addSOS( - cbc_dest, - length(tmp_model.sos1_starts) - 1, - tmp_model.sos1_starts, - tmp_model.sos1_indices, - tmp_model.sos1_weights, - Cint(1), - ) - end - if length(tmp_model.sos2_starts) > 0 - push!(tmp_model.sos2_starts, Cint(length(tmp_model.sos2_weights))) - Cbc_addSOS( - cbc_dest, - length(tmp_model.sos2_starts) - 1, - tmp_model.sos2_starts, - tmp_model.sos2_indices, - tmp_model.sos2_weights, - Cint(2), - ) - end - nsos = length(tmp_model.sos1_starts) + length(tmp_model.sos2_starts) - if nsos > 0 && Cbc_getNumIntegers(cbc_dest) == 0 - @warn( - "There are known correctness issues using Cbc with SOS " * - "constraints and no binary variables.", - ) - end - return mapping -end - ### ### Optimize and post-optimize functions ### From 8f84f30511072cb57903b3cfa48be1feb405b6be Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 2 Mar 2022 16:01:27 +1300 Subject: [PATCH 2/8] Fixes --- src/MOI_wrapper/MOI_wrapper.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index d5ef88b..8d22b3a 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -16,8 +16,6 @@ MOI.Utilities.@struct_of_constraints_by_set_types( MOI.GreaterThan{T}, MOI.Interval{T}, }, - MOI.ZeroOne, - MOI.Integer, MOI.SOS1{T}, MOI.SOS2{T}, ) @@ -37,8 +35,6 @@ const OptimizerCache = MOI.Utilities.GenericModel{ MOI.Utilities.Hyperrectangle{Float64}, _LPProductOfSets{Float64}, }, - MOI.Utilities.VectorOfConstraints{MOI.VariableIndex,MOI.ZeroOne}, - MOI.Utilities.VectorOfConstraints{MOI.VariableIndex,MOI.Integer}, MOI.Utilities.VectorOfConstraints{ MOI.VectorOfVariables, MOI.SOS1{Float64}, @@ -323,6 +319,7 @@ function MOI.copy_to(dest::Optimizer, src::OptimizerCache) end N = Cint(length(starts)) if N > 0 + push!(starts, length(weights)) Cbc_addSOS(dest, N, starts, indices, weights, Cint(type)) end end From e51f83c5f7e3ab6b118cf6a1d0acb5e36ccd6f88 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 2 Mar 2022 16:02:19 +1300 Subject: [PATCH 3/8] Fix formatting --- src/MOI_wrapper/MOI_wrapper.jl | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index 8d22b3a..f5e760e 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -10,12 +10,7 @@ MOI.Utilities.@product_of_sets( MOI.Utilities.@struct_of_constraints_by_set_types( _CbcConstraints, - Union{ - MOI.EqualTo{T}, - MOI.LessThan{T}, - MOI.GreaterThan{T}, - MOI.Interval{T}, - }, + Union{MOI.EqualTo{T},MOI.LessThan{T},MOI.GreaterThan{T},MOI.Interval{T}}, MOI.SOS1{T}, MOI.SOS2{T}, ) @@ -176,7 +171,6 @@ function MOI.is_empty(model::Optimizer) return Cbc_getNumCols(model) == 0 && Cbc_getNumRows(model) == 0 end - function _index_map( src::OptimizerCache, ::MOI.IndexMap, From 06d1803f3b4d25fdb19bab363eee9560f4e23a21 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 2 Mar 2022 16:22:40 +1300 Subject: [PATCH 4/8] Update --- src/MOI_wrapper/MOI_wrapper.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index f5e760e..761f449 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -240,13 +240,13 @@ end function _constraint_matrix(::Nothing, n) return ( - m = 0, - n = n, - colptr = Cint[], + m = Cint(0), + n = Cint(n), + colptr = fill(Cint(0), n + 1), rowval = Cint[], - nzval = Cdouble[], - lower = Cdouble[], - upper = Cdouble[], + nzval = Float64[], + lower = Float64[], + upper = Float64[], ) end @@ -303,7 +303,7 @@ function MOI.copy_to(dest::Optimizer, src::OptimizerCache) attr = MOI.ListOfConstraintIndices{MOI.VectorOfVariables,S}() for ci in MOI.get(src, attr) any_sos = true - push!(starts, length(weights)) + push!(starts, Cint(length(weights))) f = MOI.get(src, MOI.ConstraintFunction(), ci) for x in f.variables push!(indices, Cint(x.value - 1)) From d7a9114bcc0565dd0f42d5637443a052adf85f9f Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 2 Mar 2022 16:39:03 +1300 Subject: [PATCH 5/8] More updates --- test/MOI_wrapper.jl | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index b7b6f66..7ba1c79 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -28,12 +28,9 @@ function test_supports_incremental_interface() end function test_runtests() - model = MOI.Bridges.full_bridge_optimizer( - MOI.Utilities.CachingOptimizer( - MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), - Cbc.Optimizer(), - ), - Float64, + model = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + MOI.instantiate(Cbc.Optimizer; with_bridge_type = Float64), ) MOI.set(model, MOI.Silent(), true) MOI.Test.runtests( @@ -47,14 +44,8 @@ function test_runtests() ], ), exclude = [ - # TODO(odow): bug in Cbc.jl - "test_model_copy_to_UnsupportedAttribute", - "test_model_ModelFilter_AbstractConstraintAttribute", - # TODO(odow): bug in MOI - "test_model_LowerBoundAlreadySet", - "test_model_UpperBoundAlreadySet", # TODO(odow): upstream bug in Cbc - "_Indicator_", + "test_linear_Indicator_", "test_linear_SOS1_integration", "test_linear_SOS2_integration", "test_solve_SOS2_add_and_delete", From 9dfc20211af1c0793451e750f027bf83c1a0c9a3 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 3 Mar 2022 10:14:35 +1300 Subject: [PATCH 6/8] Add tests for VariablePrimalStart --- src/MOI_wrapper/MOI_wrapper.jl | 17 +++++++++-- test/MOI_wrapper.jl | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/MOI_wrapper/MOI_wrapper.jl b/src/MOI_wrapper/MOI_wrapper.jl index 761f449..3b47923 100644 --- a/src/MOI_wrapper/MOI_wrapper.jl +++ b/src/MOI_wrapper/MOI_wrapper.jl @@ -330,8 +330,19 @@ function MOI.copy_to( dest::Optimizer, src::MOI.Utilities.UniversalFallback{OptimizerCache}, ) - MOI.Utilities.throw_unsupported(src) - return MOI.copy_to(dest, src.model) + attr = MOI.VariablePrimalStart() + MOI.Utilities.throw_unsupported( + src; + excluded_attributes = Any[MOI.VariablePrimalStart()], + ) + index_map = MOI.copy_to(dest, src.model) + if attr in MOI.get(src, MOI.ListOfVariableAttributesSet()) + for (x_src, x_dest) in index_map.var_map + value = MOI.get(src, attr, x_src) + MOI.set(dest, attr, x_dest, value) + end + end + return index_map end function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) @@ -477,7 +488,7 @@ function MOI.get(model::Optimizer, ::MOI.ObjectiveBound) return Cbc_getBestPossibleObjValue(model) + model.objective_constant end -function MOI.get(model::Optimizer, ::MOI.NodeCount) +function MOI.get(model::Optimizer, ::MOI.NodeCount)::Int64 return Cbc_getNodeCount(model) end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 7ba1c79..d98d649 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -298,6 +298,61 @@ function test_SOS2() return end +""" + test_VariablePrimalStart() + +Testing that VariablePrimalStart is actually applied is a little convoluted. + +We formulate a MIP with various setttings turned off to avoid a trivial solve in +presolve. + +Then we solve and return the optimal primal solution and the number of nodes +visited. + +For the second pass, we rebuild the same MIP, but this time we pass the optimal +solution as the VariablePrimalStart, and we set maxSol=1 to force Cbc to exit +after finding a single solution. Because we passed a primal feasible point, it +should return the optimal solution after exploring 0 nodes. +""" +function test_VariablePrimalStart() + function formulate_and_solve(start) + model = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + MOI.instantiate(Cbc.Optimizer; with_bridge_type = Float64), + ) + MOI.set(model, MOI.RawOptimizerAttribute("presolve"), "off") + MOI.set(model, MOI.RawOptimizerAttribute("cuts"), "off") + MOI.set(model, MOI.RawOptimizerAttribute("heur"), "off") + MOI.set(model, MOI.RawOptimizerAttribute("logLevel"), 0) + N = 100 + x = MOI.add_variables(model, N) + MOI.add_constraint.(model, x, MOI.ZeroOne()) + w = [1 + sin(i) for i in 1:N] + c = [1 + cos(i) for i in 1:N] + if start !== nothing + MOI.set(model, MOI.RawOptimizerAttribute("maxSol"), 1) + MOI.set.(model, MOI.VariablePrimalStart(), x, start) + end + MOI.add_constraint( + model, + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(w, x), 0.0), + MOI.LessThan(10.0), + ) + obj = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(c, x), 0.0) + MOI.set(model, MOI.ObjectiveFunction{typeof(obj)}(), obj) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + MOI.optimize!(model) + sol = MOI.get.(model, MOI.VariablePrimal(), x) + return sol, MOI.get(model, MOI.NodeCount()) + end + x, nodes = formulate_and_solve(nothing) + y, nodes_start = formulate_and_solve(x) + @test x == y + @test nodes > 0 + @test nodes_start == 0 + return +end + end TestMOIWrapper.runtests() From 3f2469dbf98cf7da07f416d91bb91ff41f12f71e Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 3 Mar 2022 10:57:09 +1300 Subject: [PATCH 7/8] Fix scoping issue --- test/MOI_wrapper.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index d98d649..e29b470 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -345,9 +345,9 @@ function test_VariablePrimalStart() sol = MOI.get.(model, MOI.VariablePrimal(), x) return sol, MOI.get(model, MOI.NodeCount()) end - x, nodes = formulate_and_solve(nothing) - y, nodes_start = formulate_and_solve(x) - @test x == y + x_sol, nodes = formulate_and_solve(nothing) + y_sol, nodes_start = formulate_and_solve(x) + @test x_sol == y_sol @test nodes > 0 @test nodes_start == 0 return From 55afe91d3c648389393a276f86915d80ed8e903d Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 3 Mar 2022 11:05:22 +1300 Subject: [PATCH 8/8] Fix --- test/MOI_wrapper.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index e29b470..ba0e343 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -346,7 +346,7 @@ function test_VariablePrimalStart() return sol, MOI.get(model, MOI.NodeCount()) end x_sol, nodes = formulate_and_solve(nothing) - y_sol, nodes_start = formulate_and_solve(x) + y_sol, nodes_start = formulate_and_solve(x_sol) @test x_sol == y_sol @test nodes > 0 @test nodes_start == 0