diff --git a/src/Benchmarks/Benchmarks.jl b/src/Benchmarks/Benchmarks.jl index 9efa017405..a31ae9f86c 100644 --- a/src/Benchmarks/Benchmarks.jl +++ b/src/Benchmarks/Benchmarks.jl @@ -232,4 +232,25 @@ end return model end +@add_benchmark function copy_model(new_model) + model = new_model() + index = MOI.add_variables(model, 1_000) + cons = Vector{ + MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}} + }(undef, 1_000) + for (i, x) in enumerate(index) + cons[i] = MOI.add_constraint( + model, + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0), + MOI.LessThan(1.0 * i) + ) + end + + model2 = new_model() + MOI.copy_to(model2, model) + # MOI.copy_to(model2, model, filter_constraints=(x) -> x in cons[1:500]) + + return model2 +end + end diff --git a/src/Utilities/copy.jl b/src/Utilities/copy.jl index 91736a80fa..7172bbec32 100644 --- a/src/Utilities/copy.jl +++ b/src/Utilities/copy.jl @@ -2,19 +2,25 @@ """ automatic_copy_to(dest::MOI.ModelLike, src::MOI.ModelLike; - copy_names::Bool=true) + copy_names::Bool=true, + filter_constraints::Union{Nothing, Function}=nothing) Use [`Utilities.supports_default_copy_to`](@ref) and [`Utilities.supports_allocate_load`](@ref) to automatically choose between [`Utilities.default_copy_to`](@ref) or [`Utilities.allocate_load`](@ref) to apply the copy operation. + +If the `filter_constraints` arguments is given, only the constraints for which +this function returns `true` will be copied. This function is given a +constraint index as argument. """ function automatic_copy_to(dest::MOI.ModelLike, src::MOI.ModelLike; - copy_names::Bool=true) + copy_names::Bool=true, + filter_constraints::Union{Nothing, Function}=nothing) if supports_default_copy_to(dest, copy_names) - default_copy_to(dest, src, copy_names) + default_copy_to(dest, src, copy_names, filter_constraints) elseif supports_allocate_load(dest, copy_names) - allocate_load(dest, src, copy_names) + allocate_load(dest, src, copy_names, filter_constraints) else error("Model $(typeof(dest)) does not support copy", copy_names ? " with names" : "", ".") @@ -108,7 +114,7 @@ Base.haskey(idxmap::IndexMap, vi::MOI.VariableIndex) = haskey(idxmap.varmap, vi) Base.keys(idxmap::IndexMap) = Iterators.flatten((keys(idxmap.varmap), keys(idxmap.conmap))) Base.length(idxmap::IndexMap) = length(idxmap.varmap) + length(idxmap.conmap) -Base.iterate(idxmap::MOIU.IndexMap, args...) = iterate(Base.Iterators.flatten((idxmap.varmap, idxmap.conmap)), args...) +Base.iterate(idxmap::IndexMap, args...) = iterate(Base.Iterators.flatten((idxmap.varmap, idxmap.conmap)), args...) """ pass_attributes(dest::MOI.ModelLike, src::MOI.ModelLike, copy_names::Bool, idxmap::IndexMap, pass_attr::Function=MOI.set) @@ -242,16 +248,30 @@ end """ copy_constraints(dest::MOI.ModelLike, src::MOI.ModelLike, idxmap::IndexMap, - cis_src::Vector{<:MOI.ConstraintIndex}) + cis_src::Vector{<:MOI.ConstraintIndex}, + filter_constraints::Union{Nothing, Function}=nothing) Copy the constraints `cis_src` from the model `src` to the model `dest` and fill `idxmap` accordingly. Note that the attributes are not copied; call [`pass_attributes`] to copy the constraint attributes. + +If the `filter_constraints` arguments is given, only the constraints for which +this function returns `true` will be copied. This function is given a +constraint index as argument. """ function copy_constraints(dest::MOI.ModelLike, src::MOI.ModelLike, idxmap::IndexMap, - cis_src::Vector{<:MOI.ConstraintIndex}) + cis_src::Vector{<:MOI.ConstraintIndex}, + filter_constraints::Union{Nothing, Function}=nothing) + # Retrieve the constraints to copy. f_src = MOI.get(src, MOI.ConstraintFunction(), cis_src) + + # Filter this set of constraints if needed before. + if filter_constraints !== nothing + filter!(filter_constraints, cis_src) + end + + # Copy the constraints into the new model and build the map. f_dest = map_indices.(Ref(idxmap), f_src) s = MOI.get(src, MOI.ConstraintSet(), cis_src) cis_dest = MOI.add_constraints(dest, f_dest, s) @@ -264,23 +284,41 @@ function pass_constraints( dest::MOI.ModelLike, src::MOI.ModelLike, copy_names::Bool, idxmap::IndexMap, single_variable_types, single_variable_indices, vector_of_variables_types, vector_of_variables_indices, - pass_cons=copy_constraints, pass_attr=MOI.set + pass_cons=copy_constraints, pass_attr=MOI.set; + filter_constraints::Union{Nothing, Function}=nothing ) + # copy_constraints can also take a filter_constraints argument; however, filtering + # is performed within this function (because it also calls MOI.set on the constraints). + # Don't pass this argument to copy_constraints/pass_cons to avoid a double filtering. for (S, cis_src) in zip(single_variable_types, single_variable_indices) + if filter_constraints !== nothing + filter!(filter_constraints, cis_src) + end # do the rest in `pass_cons` which is type stable pass_cons(dest, src, idxmap, cis_src) + cis_src = MOI.get(src, MOI.ListOfConstraintIndices{ MOI.SingleVariable, S}()) + if filter_constraints !== nothing + filter!(filter_constraints, cis_src) + end pass_attributes(dest, src, copy_names, idxmap, cis_src, pass_attr) end for (S, cis_src) in zip(vector_of_variables_types, vector_of_variables_indices) + if filter_constraints !== nothing + filter!(filter_constraints, cis_src) + end # do the rest in `pass_cons` which is type stable pass_cons(dest, src, idxmap, cis_src) + cis_src = MOI.get(src, MOI.ListOfConstraintIndices{ MOI.VectorOfVariables, S}()) + if filter_constraints !== nothing + filter!(filter_constraints, cis_src) + end pass_attributes(dest, src, copy_names, idxmap, cis_src, pass_attr) end @@ -290,6 +328,9 @@ function pass_constraints( ] for (F, S) in nonvariable_constraint_types cis_src = MOI.get(src, MOI.ListOfConstraintIndices{F, S}()) + if filter_constraints !== nothing + filter!(filter_constraints, cis_src) + end # do the rest in `pass_cons` which is type stable pass_cons(dest, src, idxmap, cis_src) pass_attributes(dest, src, copy_names, idxmap, cis_src, pass_attr) @@ -317,14 +358,21 @@ function default_copy_to(dest::MOI.ModelLike, src::MOI.ModelLike) end """ - default_copy_to(dest::MOI.ModelLike, src::MOI.ModelLike, copy_names::Bool) + default_copy_to(dest::MOI.ModelLike, src::MOI.ModelLike, copy_names::Bool, + filter_constraints::Union{Nothing, Function}=nothing) Implements `MOI.copy_to(dest, src)` by adding the variables and then the constraints and attributes incrementally. The function [`supports_default_copy_to`](@ref) can be used to check whether `dest` supports the copying a model incrementally. + +If the `filter_constraints` arguments is given, only the constraints for which +this function returns `true` will be copied. This function is given a +constraint index as argument. """ -function default_copy_to(dest::MOI.ModelLike, src::MOI.ModelLike, copy_names::Bool) +function default_copy_to(dest::MOI.ModelLike, src::MOI.ModelLike, + copy_names::Bool, + filter_constraints::Union{Nothing, Function}=nothing) MOI.empty!(dest) vis_src = MOI.get(src, MOI.ListOfVariableIndices()) @@ -378,7 +426,8 @@ function default_copy_to(dest::MOI.ModelLike, src::MOI.ModelLike, copy_names::Bo # Copy constraints pass_constraints(dest, src, copy_names, idxmap, single_variable_types, single_variable_not_added, - vector_of_variables_types, vector_of_variables_not_added) + vector_of_variables_types, vector_of_variables_not_added, + filter_constraints=filter_constraints) return idxmap end @@ -678,13 +727,20 @@ function load_constraints(dest::MOI.ModelLike, src::MOI.ModelLike, end """ - allocate_load(dest::MOI.ModelLike, src::MOI.ModelLike) + allocate_load(dest::MOI.ModelLike, src::MOI.ModelLike, + filter_constraints::Union{Nothing, Function}=nothing + ) Implements `MOI.copy_to(dest, src)` using the Allocate-Load API. The function [`supports_allocate_load`](@ref) can be used to check whether `dest` supports the Allocate-Load API. + +If the `filter_constraints` arguments is given, only the constraints for which +this function returns `true` will be copied. This function is given a +constraint index as argument. """ -function allocate_load(dest::MOI.ModelLike, src::MOI.ModelLike, copy_names::Bool) +function allocate_load(dest::MOI.ModelLike, src::MOI.ModelLike, copy_names::Bool, + filter_constraints::Union{Nothing, Function}=nothing) MOI.empty!(dest) vis_src = MOI.get(src, MOI.ListOfVariableIndices()) @@ -722,7 +778,8 @@ function allocate_load(dest::MOI.ModelLike, src::MOI.ModelLike, copy_names::Bool pass_constraints(dest, src, copy_names, idxmap, single_variable_types, single_variable_not_allocated, vector_of_variables_types, vector_of_variables_not_allocated, - allocate_constraints, allocate) + allocate_constraints, allocate, + filter_constraints=filter_constraints) # Load variables load_variables(dest, length(vis_src)) @@ -743,7 +800,7 @@ function allocate_load(dest::MOI.ModelLike, src::MOI.ModelLike, copy_names::Bool pass_constraints(dest, src, copy_names, idxmap, single_variable_types, single_variable_not_allocated, vector_of_variables_types, vector_of_variables_not_allocated, - load_constraints, load) + load_constraints, load, filter_constraints=filter_constraints) return idxmap end diff --git a/test/Utilities/copy.jl b/test/Utilities/copy.jl index 549668d9a8..ea79c4c2ca 100644 --- a/test/Utilities/copy.jl +++ b/test/Utilities/copy.jl @@ -256,3 +256,62 @@ MOI.supports_add_constrained_variable(::ReverseOrderConstrainedVariablesModel, : @test typeof(c1) == typeof(dest.constraintIndices[1]) @test typeof(c2) == typeof(dest.constraintIndices[2]) end + +@testset "Filtering copy: check based on index" begin + # Create a basic model. + src = MOIU.Model{Float64}() + x = MOI.add_variable(src) + c1 = MOI.add_constraint(src, x, MOI.LessThan{Float64}(1.0)) + c2 = MOI.add_constraint(src, x, MOI.GreaterThan{Float64}(0.0)) + + # Filtering function: the default case where this function always returns + # true is already well-tested by the above cases. + # Only keep the constraint c1. + f = (cidx) -> cidx == c1 + + # Perform the copy. + dst = OrderConstrainedVariablesModel() + index_map = MOI.copy_to(dst, src, filter_constraints=f) + + @test typeof(c1) == typeof(dst.constraintIndices[1]) + @test length(dst.constraintIndices) == 1 +end + +mutable struct BoundModel <: MOI.ModelLike + # Type of model that only supports ≤ bound constraints. In particular, it does not support integrality constraints. + inner::MOIU.Model{Float64} + BoundModel() = new(MOIU.Model{Float64}()) +end + +MOI.add_variable(model::BoundModel) = MOI.add_variable(model.inner) +MOI.add_constraint(model::BoundModel, f::MOI.AbstractFunction, s::MOI.LessThan{Float64}) = MOI.add_constraint(model.inner, f, s) +MOI.supports_constraint(::BoundModel, ::Type{MOI.SingleVariable}, ::MOI.LessThan{Float64}) = true + +MOIU.supports_default_copy_to(::BoundModel, ::Bool) = true +MOI.copy_to(dest::BoundModel, src::MOI.ModelLike; kws...) = MOIU.automatic_copy_to(dest, src; kws...) +MOI.empty!(model::BoundModel) = MOI.empty!(model.inner) + +MOI.supports(::BoundModel, ::Type{MOI.NumberOfConstraints}) = true +MOI.get(model::BoundModel, attr::MOI.NumberOfConstraints) = MOI.get(model.inner, attr) + +@testset "Filtering copy: check based on constraint type" begin + # Create a basic model. + src = MOIU.Model{Float64}() + x = MOI.add_variable(src) + c1 = MOI.add_constraint(src, x, MOI.LessThan{Float64}(10.0)) + c2 = MOI.add_constraint(src, x, MOI.Integer()) + + # Filtering function: get rid of integrality constraint. + f = (cidx) -> MOI.get(src, MOI.ConstraintSet(), cidx) != MOI.Integer() + + # Perform the unfiltered copy. This should throw an error (i.e. the implementation of BoundModel + # should be correct). + dst = BoundModel() + @test_throws MOI.UnsupportedConstraint{MOI.SingleVariable, MOI.Integer} MOI.copy_to(dst, src) + + # Perform the filtered copy. This should not throw an error. + dst = BoundModel() + MOI.copy_to(dst, src, filter_constraints=f) + @test MOI.get(dst, MOI.NumberOfConstraints{MOI.SingleVariable, MOI.LessThan{Float64}}()) == 1 + @test MOI.get(dst, MOI.NumberOfConstraints{MOI.SingleVariable, MOI.Integer}()) == 0 +end