diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 7599be8b02..3949f148e4 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -320,6 +320,41 @@ function MOI.supports( return MOI.supports(b.model, attr) end +function MOIU.pass_nonvariable_constraints( + dest::AbstractBridgeOptimizer, + src::MOI.ModelLike, + idxmap::MOIU.IndexMap, + constraint_types, + pass_cons; + filter_constraints::Union{Nothing,Function} = nothing, +) + not_bridged_types = eltype(constraint_types)[] + bridged_types = eltype(constraint_types)[] + for (F, S) in constraint_types + if is_bridged(dest, F, S) + push!(bridged_types, (F, S)) + else + push!(not_bridged_types, (F, S)) + end + end + MOIU.pass_nonvariable_constraints( + dest.model, + src, + idxmap, + not_bridged_types, + pass_cons; + filter_constraints = filter_constraints, + ) + return MOIU.pass_nonvariable_constraints_fallback( + dest, + src, + idxmap, + bridged_types, + pass_cons; + filter_constraints = filter_constraints, + ) +end + function MOI.copy_to(mock::AbstractBridgeOptimizer, src::MOI.ModelLike; kws...) return MOIU.automatic_copy_to(mock, src; kws...) end diff --git a/src/Test/modellike.jl b/src/Test/modellike.jl index b1bc9e82d8..9d51eda101 100644 --- a/src/Test/modellike.jl +++ b/src/Test/modellike.jl @@ -783,7 +783,7 @@ function start_values_test(dest::MOI.ModelLike, src::MOI.ModelLike) end end -function copytest(dest::MOI.ModelLike, src::MOI.ModelLike) +function copytest(dest::MOI.ModelLike, src::MOI.ModelLike; copy_names = false) @test MOIU.supports_default_copy_to(src, true) #=copy_names=# MOI.empty!(src) MOI.empty!(dest) @@ -850,13 +850,15 @@ function copytest(dest::MOI.ModelLike, src::MOI.ModelLike) MOI.Zeros, ) - dict = MOI.copy_to(dest, src, copy_names = false) + dict = MOI.copy_to(dest, src, copy_names = copy_names) - @test !MOI.supports(dest, MOI.Name()) || MOI.get(dest, MOI.Name()) == "" + dest_name(src_name) = copy_names ? src_name : "" + @test !MOI.supports(dest, MOI.Name()) || + MOI.get(dest, MOI.Name()) == dest_name("ModelName") @test MOI.get(dest, MOI.NumberOfVariables()) == 4 if MOI.supports(dest, MOI.VariableName(), MOI.VariableIndex) - for vi in v - MOI.get(dest, MOI.VariableName(), dict[vi]) == "" + for i in eachindex(v) + MOI.get(dest, MOI.VariableName(), dict[v[i]]) == dest_name("var$i") end end @test MOI.get( @@ -908,17 +910,17 @@ function copytest(dest::MOI.ModelLike, src::MOI.ModelLike) @test (MOI.VectorAffineFunction{Float64}, MOI.Zeros) in loc @test !MOI.supports(dest, MOI.ConstraintName(), typeof(csv)) || - MOI.get(dest, MOI.ConstraintName(), dict[csv]) == "" + MOI.get(dest, MOI.ConstraintName(), dict[csv]) == dest_name("csv") @test MOI.get(dest, MOI.ConstraintFunction(), dict[csv]) == MOI.SingleVariable(dict[w]) @test MOI.get(dest, MOI.ConstraintSet(), dict[csv]) == MOI.EqualTo(2.0) @test !MOI.supports(dest, MOI.ConstraintName(), typeof(cvv)) || - MOI.get(dest, MOI.ConstraintName(), dict[cvv]) == "" + MOI.get(dest, MOI.ConstraintName(), dict[cvv]) == dest_name("cvv") @test MOI.get(dest, MOI.ConstraintFunction(), dict[cvv]) == MOI.VectorOfVariables(getindex.(Ref(dict), v)) @test MOI.get(dest, MOI.ConstraintSet(), dict[cvv]) == MOI.Nonnegatives(3) @test !MOI.supports(dest, MOI.ConstraintName(), typeof(csa)) || - MOI.get(dest, MOI.ConstraintName(), dict[csa]) == "" + MOI.get(dest, MOI.ConstraintName(), dict[csa]) == dest_name("csa") @test MOI.get(dest, MOI.ConstraintFunction(), dict[csa]) ≈ MOI.ScalarAffineFunction( MOI.ScalarAffineTerm.([1.0, 3.0], [dict[v[3]], dict[v[1]]]), @@ -926,7 +928,7 @@ function copytest(dest::MOI.ModelLike, src::MOI.ModelLike) ) @test MOI.get(dest, MOI.ConstraintSet(), dict[csa]) == MOI.LessThan(2.0) @test !MOI.supports(dest, MOI.ConstraintName(), typeof(cva)) || - MOI.get(dest, MOI.ConstraintName(), dict[cva]) == "" + MOI.get(dest, MOI.ConstraintName(), dict[cva]) == dest_name("cva") @test MOI.get(dest, MOI.ConstraintFunction(), dict[cva]) ≈ MOI.VectorAffineFunction( MOI.VectorAffineTerm.( diff --git a/src/Utilities/cachingoptimizer.jl b/src/Utilities/cachingoptimizer.jl index 58c2b4b4a0..4c721ddebc 100644 --- a/src/Utilities/cachingoptimizer.jl +++ b/src/Utilities/cachingoptimizer.jl @@ -207,7 +207,8 @@ function _standardize(d::IndexMap) end function MOI.copy_to(m::CachingOptimizer, src::MOI.ModelLike; kws...) - return automatic_copy_to(m, src; kws...) + m.state == ATTACHED_OPTIMIZER && reset_optimizer(m) + return MOI.copy_to(m.model_cache, src; kws...) end function supports_default_copy_to(model::CachingOptimizer, copy_names::Bool) return supports_default_copy_to(model.model_cache, copy_names) diff --git a/src/Utilities/copy.jl b/src/Utilities/copy.jl index 8b73d4cd40..74995ac7af 100644 --- a/src/Utilities/copy.jl +++ b/src/Utilities/copy.jl @@ -422,6 +422,62 @@ function copy_constraints( end end +function pass_nonvariable_constraints_fallback( + dest::MOI.ModelLike, + src::MOI.ModelLike, + idxmap::IndexMap, + constraint_types, + pass_cons = copy_constraints; + filter_constraints::Union{Nothing,Function} = nothing, +) + for (F, S) in 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) + end +end + +""" + pass_nonvariable_constraints( + dest::MOI.ModelLike, + src::MOI.ModelLike, + idxmap::IndexMap, + constraint_types, + pass_cons = copy_constraints; + filter_constraints::Union{Nothing,Function} = nothing, + ) + +For all tuples `(F, S)` in `constraint_types`, copy all constraints of type +`F`-in-`S` from `src` to `dest` mapping the variables indices with `idxmap`. +If `filter_constraints` is not nothing, only indices `ci` such that +`filter_constraints(ci)` is true are copied. + +The default implementation calls `pass_nonvariable_constraints_fallback` which +copies the constraints with `pass_cons` and their attributes with `pass_attr`. +A method can be implemented to use a specialized copy for a given type of +`dest`. +""" +function pass_nonvariable_constraints( + dest::MOI.ModelLike, + src::MOI.ModelLike, + idxmap::IndexMap, + constraint_types, + pass_cons = copy_constraints; + filter_constraints::Union{Nothing,Function} = nothing, +) + return pass_nonvariable_constraints_fallback( + dest, + src, + idxmap, + constraint_types, + pass_cons; + filter_constraints = filter_constraints, + ) +end + function pass_constraints( dest::MOI.ModelLike, src::MOI.ModelLike, @@ -473,13 +529,20 @@ function pass_constraints( (F, S) for (F, S) in MOI.get(src, MOI.ListOfConstraints()) if F != MOI.SingleVariable && F != MOI.VectorOfVariables ] + pass_nonvariable_constraints( + dest, + src, + idxmap, + nonvariable_constraint_types, + pass_cons; + filter_constraints = filter_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) end end diff --git a/src/Utilities/model.jl b/src/Utilities/model.jl index 8b8c6e859e..f9c7aaa0dc 100644 --- a/src/Utilities/model.jl +++ b/src/Utilities/model.jl @@ -752,6 +752,24 @@ function MOI.empty!(model::AbstractModel{T}) where {T} return end +function pass_nonvariable_constraints( + dest::AbstractModel, + src::MOI.ModelLike, + idxmap::IndexMap, + constraint_types, + pass_cons; + filter_constraints::Union{Nothing,Function} = nothing, +) + return pass_nonvariable_constraints( + dest.constraints, + src, + idxmap, + constraint_types, + pass_cons; + filter_constraints = filter_constraints, + ) +end + function MOI.copy_to(dest::AbstractModel, src::MOI.ModelLike; kws...) return automatic_copy_to(dest, src; kws...) end diff --git a/src/Utilities/universalfallback.jl b/src/Utilities/universalfallback.jl index 892e6bfc17..e23bfcf611 100644 --- a/src/Utilities/universalfallback.jl +++ b/src/Utilities/universalfallback.jl @@ -86,6 +86,41 @@ function MOI.empty!(uf::UniversalFallback) return end +function pass_nonvariable_constraints( + dest::UniversalFallback, + src::MOI.ModelLike, + idxmap::IndexMap, + constraint_types, + pass_cons; + filter_constraints::Union{Nothing,Function} = nothing, +) + supported_types = eltype(constraint_types)[] + unsupported_types = eltype(constraint_types)[] + for (F, S) in constraint_types + if MOI.supports_constraint(dest.model, F, S) + push!(supported_types, (F, S)) + else + push!(unsupported_types, (F, S)) + end + end + pass_nonvariable_constraints( + dest.model, + src, + idxmap, + supported_types, + pass_cons; + filter_constraints = filter_constraints, + ) + return pass_nonvariable_constraints_fallback( + dest, + src, + idxmap, + unsupported_types, + pass_cons; + filter_constraints = filter_constraints, + ) +end + function MOI.copy_to(uf::UniversalFallback, src::MOI.ModelLike; kws...) return MOIU.automatic_copy_to(uf, src; kws...) end diff --git a/test/Utilities/copy.jl b/test/Utilities/copy.jl index 5ac74e9afb..4e45549330 100644 --- a/test/Utilities/copy.jl +++ b/test/Utilities/copy.jl @@ -45,7 +45,8 @@ end MOIT.failcopytestia(model) MOIT.failcopytestva(model) MOIT.failcopytestca(model) - MOIT.copytest(model, MOIU.Model{Float64}()) + MOIT.copytest(model, MOIU.Model{Float64}(), copy_names = false) + MOIT.copytest(model, MOIU.Model{Float64}(), copy_names = true) end @testset "Allocate-Load" begin @test !MOIU.supports_allocate_load(DummyModel(), false) @@ -55,7 +56,8 @@ end MOIT.failcopytestia(mock) MOIT.failcopytestva(mock) MOIT.failcopytestca(mock) - MOIT.copytest(mock, MOIU.Model{Float64}()) + MOIT.copytest(mock, MOIU.Model{Float64}(), copy_names = false) + MOIT.copytest(mock, MOIU.Model{Float64}(), copy_names = true) end struct DummyEvaluator <: MOI.AbstractNLPEvaluator end @@ -620,3 +622,73 @@ end MOI.NumberOfConstraints{MOI.SingleVariable,MOI.Integer}(), ) == 0 end + +# We create a `OnlyCopyConstraints` that don't implement `add_constraint` but +# implements `pass_nonvariable_constraints` to check that this is passed accross +# all layers without falling back to `pass_nonvariable_constraints_fallback` +# which calls `add_constraint`. + +struct OnlyCopyConstraints{F,S} <: MOI.ModelLike + constraints::MOIU.VectorOfConstraints{F,S} + function OnlyCopyConstraints{F,S}() where {F,S} + return new{F,S}(MOIU.VectorOfConstraints{F,S}()) + end +end +MOI.empty!(model::OnlyCopyConstraints) = MOI.empty!(model.constraints) +function MOI.supports_constraint( + model::OnlyCopyConstraints, + F::Type{<:MOI.AbstractFunction}, + S::Type{<:MOI.AbstractSet}, +) + return MOI.supports_constraint(model.constraints, F, S) +end +function MOIU.pass_nonvariable_constraints( + dest::OnlyCopyConstraints, + src::MOI.ModelLike, + idxmap::MOIU.IndexMap, + constraint_types, + pass_cons; + filter_constraints::Union{Nothing,Function} = nothing, +) + return MOIU.pass_nonvariable_constraints( + dest.constraints, + src, + idxmap, + constraint_types, + pass_cons; + filter_constraints = filter_constraints, + ) +end + +function test_pass_copy(::Type{T}) where {T} + F = MOI.ScalarAffineFunction{T} + S = MOI.EqualTo{T} + S2 = MOI.GreaterThan{T} + src = MOIU.Model{T}() + x = MOI.add_variable(src) + fx = MOI.SingleVariable(x) + MOI.add_constraint(src, T(1) * fx, MOI.EqualTo(T(1))) + MOI.add_constraint(src, T(2) * fx, MOI.EqualTo(T(2))) + MOI.add_constraint(src, T(3) * fx, MOI.GreaterThan(T(3))) + MOI.add_constraint(src, T(4) * fx, MOI.GreaterThan(T(4))) + dest = MOIU.CachingOptimizer( + MOI.Bridges.full_bridge_optimizer( + MOIU.UniversalFallback( + MOIU.GenericOptimizer{T,OnlyCopyConstraints{F,S}}(), + ), + T, + ), + MOIU.AUTOMATIC, + ) + MOI.copy_to(dest, src) + voc = dest.model_cache.model.model.constraints.constraints + @test MOI.get(voc, MOI.NumberOfConstraints{F,S}()) == 2 + @test !haskey(dest.model_cache.model.constraints, (F, S)) + @test MOI.get(dest, MOI.NumberOfConstraints{F,S2}()) == 2 + @test haskey(dest.model_cache.model.constraints, (F, S2)) +end + +@testset "copy of constraints passed as copy accross layers" begin + test_pass_copy(Int) + test_pass_copy(Float64) +end