Skip to content
Merged
21 changes: 21 additions & 0 deletions src/Benchmarks/Benchmarks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
87 changes: 72 additions & 15 deletions src/Utilities/copy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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" : "", ".")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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))
Expand All @@ -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
59 changes: 59 additions & 0 deletions test/Utilities/copy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

@blegat blegat Aug 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add a test where the filter filters out all constraints of a given type and the output model does not support the type of constraints that is filtered out ? If they are all filtered out, it shouldn't throw UnsupportedConstraint but it'd be good to check with a test. This is needed in the use case of MIP relaxation where a MIP is copied to an LP that does not support integer variables.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like the last commit?
2fa662c


@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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add a @test_throws ... MOI.copy_to(dst, src) to make sure that BoundModel was setup correctly ?

@test MOI.get(dst, MOI.NumberOfConstraints{MOI.SingleVariable, MOI.LessThan{Float64}}()) == 1
@test MOI.get(dst, MOI.NumberOfConstraints{MOI.SingleVariable, MOI.Integer}()) == 0
end