diff --git a/src/JuMP.jl b/src/JuMP.jl index a0ff2f22be1..de139af1786 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -331,7 +331,6 @@ function objective_sense(model::Model) end # TODO(IainNZ): Document these too. -# TODO(#1381): Implement Base.copy for Model. object_dictionary(model::Model) = model.obj_dict termination_status(model::Model) = MOI.get(model, MOI.TerminationStatus()) primal_status(model::Model) = MOI.get(model, MOI.PrimalStatus()) @@ -748,6 +747,7 @@ struct NonlinearParameter <: AbstractJuMPScalar end ########################################################################## +include("copy.jl") include("containers.jl") include("operators.jl") include("macros.jl") diff --git a/src/copy.jl b/src/copy.jl new file mode 100644 index 00000000000..c2c9c734bd8 --- /dev/null +++ b/src/copy.jl @@ -0,0 +1,180 @@ +# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +""" + copy_extension_data(data, new_model::AbstractModel, model::AbstractModel) + +Return a copy of the extension data `data` of the model `model` to the extension +data of the new model `new_model`. A method should be added for any JuMP +extension storing data in the `ext` field. +""" +function copy_extension_data end + +""" + copy_single_variable_constraints(dest::Dict{MOI.VariableIndex, + MOICON{MOI.SingleVariable, S}}, + src::Dict{MOI.VariableIndex, + MOICON{MOI.SingleVariable, S}}, + index_map) where S + +Copy the single variable constraint indices of `src` into `dest` mapping +variable and constraint indices using `index_map`. +""" +function copy_single_variable_constraints(dest::Dict{MOI.VariableIndex, + MOICON{MOI.SingleVariable, + S}}, + src::Dict{MOI.VariableIndex, + MOICON{MOI.SingleVariable, + S}}, + index_map) where S + for (variable_index, constraint_index) in src + dest[index_map[variable_index]] = index_map[constraint_index] + end +end + +""" + ReferenceMap + +Mapping between variable and constraint reference of a model and its copy. The +reference of the copied model can be obtained by indexing the map with the +reference of the corresponding reference of the original model. +""" +struct ReferenceMap + model::Model + index_map::MOIU.IndexMap +end +function Base.getindex(reference_map::ReferenceMap, vref::VariableRef) + return VariableRef(reference_map.model, + reference_map.index_map[index(vref)]) +end +function Base.getindex(reference_map::ReferenceMap, cref::ConstraintRef) + return ConstraintRef(reference_map.model, + reference_map.index_map[index(cref)], + cref.shape) +end +if VERSION >= v"0.7-" + Base.broadcastable(reference_map::ReferenceMap) = Ref(reference_map) +end + + +""" + copy_model(model::Model) + +Return a copy of the model `model` and a [`ReferenceMap`](@ref) that can be used +to obtain the variable and constraint reference of the new model corresponding +to a given `model`'s reference. A [`Base.copy(::AbstractModel)`](@ref) method +has also been implemented, it is similar to `copy_model` but does not return +the reference map. + +## Note + +Model copy is not supported in Direct mode, i.e. when a model is constructed +using the [`direct_model`](@ref) constructor instead of the [`Model`](@ref) +constructor. Moreover, independently on whether an optimizer was provided at +model construction, the new model will have no optimizer, i.e., an optimizer +will have to be provided to the new model in the [`optimize!`](@ref) call. + +## Examples + +In the following example, a model `model` is constructed with a variable `x` and +a constraint `cref`. It is then copied into a model `new_model` with the new +references assigned to `x_new` and `cref_new`. +```julia +model = Model() +@variable(model, x) +@constraint(model, cref, x == 2) + +new_model, reference_map = JuMP.copy_model(model) +x_new = reference_map[x] +cref_new = reference_map[cref] +``` +""" +function copy_model(model::Model) + if mode(model) == Direct + error("Cannot copy a model in Direct mode. Use the `Model` constructor", + " instead of the `direct_model` constructor to be able to copy", + " the constructed model.") + end + caching_mode = caching_optimizer(model).mode + # TODO add bridges added to the bridge optimizer that are not part of the + # fullbridgeoptimizer + bridge_constraints = model.moi_backend isa MOI.Bridges.LazyBridgeOptimizer{<:MOIU.CachingOptimizer} + new_model = Model(caching_mode = caching_mode, + bridge_constraints = bridge_constraints) + + # Copy the MOI backend, note that variable and constraint indices may have + # changed, the `index_map` gives the map between the indices of + # `model.moi_backend` and the indices of `new_model.moi_backend`. + index_map = MOI.copy!(new_model.moi_backend, model.moi_backend, + copynames = true) + # TODO copynames is needed because of https://github.com/JuliaOpt/MathOptInterface.jl/issues/494 + # we can remove it when this is fixed and released + + copy_single_variable_constraints(new_model.variable_to_lower_bound, + model.variable_to_lower_bound, index_map) + copy_single_variable_constraints(new_model.variable_to_upper_bound, + model.variable_to_upper_bound, index_map) + copy_single_variable_constraints(new_model.variable_to_fix, + model.variable_to_fix, index_map) + copy_single_variable_constraints(new_model.variable_to_integrality, + model.variable_to_integrality, index_map) + copy_single_variable_constraints(new_model.variable_to_zero_one, + model.variable_to_zero_one, index_map) + + new_model.optimize_hook = model.optimize_hook + + # TODO copy NLP data + if model.nlp_data !== nothing + error("copy is not supported yet for models with nonlinear constraints", + " and/or nonlinear objective function") + end + + reference_map = ReferenceMap(new_model, index_map) + + for (name, value) in object_dictionary(model) + new_model.obj_dict[name] = getindex.(reference_map, value) + end + + for (key, data) in model.ext + new_model.ext[key] = copy_extension_data(data, new_model, model) + end + + return new_model, reference_map +end + +""" + copy(model::AbstractModel) + +Return a copy of the model `model`. It is similar to [`copy_model`](@ref) +except that it does not return the mapping between the references of `model` +and its copy. + +## Note + +Model copy is not supported in Direct mode, i.e. when a model is constructed +using the [`direct_model`](@ref) constructor instead of the [`Model`](@ref) +constructor. Moreover, independently on whether an optimizer was provided at +model construction, the new model will have no optimizer, i.e., an optimizer +will have to be provided to the new model in the [`optimize!`](@ref) call. + +## Examples + +In the following example, a model `model` is constructed with a variable `x` and +a constraint `cref`. It is then copied into a model `new_model` with the new +references assigned to `x_new` and `cref_new`. +```julia +model = Model() +@variable(model, x) +@constraint(model, cref, x == 2) + +new_model = copy(model) +x_new = model[:x] +cref_new = model[:cref] +``` +""" +function Base.copy(model::AbstractModel) + new_model, _ = copy_model(model) + return new_model +end diff --git a/src/quadexpr.jl b/src/quadexpr.jl index 12498965c7d..5a15458bf1b 100644 --- a/src/quadexpr.jl +++ b/src/quadexpr.jl @@ -230,7 +230,7 @@ end # variables to the new model's variables function Base.copy(q::GenericQuadExpr, new_model::Model) GenericQuadExpr(copy(q.qvars1, new_model), copy(q.qvars2, new_model), - copy(q.qcoeffs), copy(q.aff, new_model)) + copy(q.qcoeffs), copy(q.aff, new_model)) end # TODO: result_value for QuadExpr diff --git a/test/model.jl b/test/model.jl index 40b1f66cc59..9a61b643dc3 100644 --- a/test/model.jl +++ b/test/model.jl @@ -66,3 +66,78 @@ end @test optimizer.a == 1 @test optimizer.b == 2 end + +struct DummyExtensionData + model::JuMP.Model +end +function JuMP.copy_extension_data(data::DummyExtensionData, + new_model::JuMP.AbstractModel, + model::JuMP.AbstractModel) + @test data.model === model + return DummyExtensionData(new_model) +end +function dummy_optimizer_hook(::JuMP.AbstractModel) end + +@testset "Model copy" begin + for copy_model in (true, true) + @testset "Using $(copy_model ? "JuMP.copy_model" : "Base.copy")" begin + for caching_mode in (MOIU.Automatic, MOIU.Manual) + @testset "In $caching_mode mode" begin + for bridge_constraints in (false, true) + model = Model(caching_mode = caching_mode, + bridge_constraints = bridge_constraints) + model.optimize_hook = dummy_optimizer_hook + data = DummyExtensionData(model) + model.ext[:dummy] = data + @variable(model, x ≥ 0, Bin) + @variable(model, y ≤ 1, Int) + @variable(model, z == 0) + @constraint(model, cref, x + y == 1) + + if copy_model + new_model, reference_map = JuMP.copy_model(model) + else + new_model = copy(model) + reference_map = Dict{Union{JuMP.VariableRef, + JuMP.ConstraintRef}, + Union{JuMP.VariableRef, + JuMP.ConstraintRef}}() + reference_map[x] = new_model[:x] + reference_map[y] = new_model[:y] + reference_map[z] = new_model[:z] + reference_map[cref] = new_model[:cref] + end + @test MOIU.mode(JuMP.caching_optimizer(new_model)) == caching_mode + @test bridge_constraints == (new_model.moi_backend isa MOI.Bridges.LazyBridgeOptimizer) + @test new_model.optimize_hook === dummy_optimizer_hook + @test new_model.ext[:dummy].model === new_model + x_new = reference_map[x] + @test x_new.m === new_model + @test JuMP.name(x_new) == "x" + y_new = reference_map[y] + @test y_new.m === new_model + @test JuMP.name(y_new) == "y" + z_new = reference_map[z] + @test z_new.m === new_model + @test JuMP.name(z_new) == "z" + if copy_model + @test JuMP.LowerBoundRef(x_new) == reference_map[JuMP.LowerBoundRef(x)] + @test JuMP.BinaryRef(x_new) == reference_map[JuMP.BinaryRef(x)] + @test JuMP.UpperBoundRef(y_new) == reference_map[JuMP.UpperBoundRef(y)] + @test JuMP.IntegerRef(y_new) == reference_map[JuMP.IntegerRef(y)] + @test JuMP.FixRef(z_new) == reference_map[JuMP.FixRef(z)] + end + cref_new = reference_map[cref] + @test cref_new.m === new_model + @test JuMP.name(cref_new) == "cref" + end + end + end + end + end + @testset "In Direct mode" begin + mock = MOIU.MockOptimizer(JuMP.JuMPMOIModel{Float64}()) + model = JuMP.direct_model(mock) + @test_throws ErrorException JuMP.copy(model) + end +end