diff --git a/src/FileFormats/NL/NL.jl b/src/FileFormats/NL/NL.jl index 5c957be11b..eb984db854 100644 --- a/src/FileFormats/NL/NL.jl +++ b/src/FileFormats/NL/NL.jl @@ -773,4 +773,84 @@ function Base.write(io::IO, nlmodel::Model) return nlmodel end +# This part is needed for JuMP's write_to_file method. +# +# JuMP calls: +# ```julia +# dest = MOI.FileFormats.Model(format = format, filename = filename) +# bridged_dest = MOI.Bridges.full_bridge_optimizer(dest, Float64) +# MOI.copy_to(bridged_dest, model) +# MOI.write_to_file(dest, filename) +# ``` +# So we need a way of calling `copy_to` from a model to a bridged NL.Model. +# However because we don't support the incremental interface, this would require +# a CachingOptimizer. Except we can't use a CachingOptimizer because NL.Model +# isn't an AbstractOptimizer! +# +# The solution, at least as a temporary measure, is to write a simplified +# _CachingModel that acts as a cache during copy_to. +# +# The other FileFormats don't have this problem because they use +# Utilities.@model, which supports the incremental interface. + +struct _CachingModel{C} <: MOI.AbstractOptimizer + inner::MOI.FileFormats.NL.Model + cache::C + function _CachingModel(model::MOI.FileFormats.NL.Model) + cache = MOI.Bridges.full_bridge_optimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + Float64, + ) + return new{typeof(cache)}(model, cache) + end +end + +MOI.supports_incremental_interface(::_CachingModel) = true + +MOI.is_empty(model::_CachingModel) = MOI.is_empty(model.cache) + +MOI.empty!(model::_CachingModel) = MOI.empty!(model.cache) + +function MOI.add_constraint( + model::_CachingModel, + f::MOI.AbstractFunction, + s::MOI.AbstractSet, +) + return MOI.add_constraint(model.cache, f, s) +end + +MOI.add_variable(model::_CachingModel) = MOI.add_variable(model.cache) + +function MOI.set(model::_CachingModel, attr::MOI.AnyAttribute, args...) + return MOI.set(model.cache, attr, args...) +end + +function MOI.get(model::_CachingModel, attr::MOI.AnyAttribute, args...) + return MOI.get(model.cache, attr, args...) +end + +function MOI.supports(model::_CachingModel, attr::MOI.AnyAttribute, args...) + return MOI.supports(model.inner, attr, args...) +end + +function MOI.supports_constraint( + model::_CachingModel, + f::MOI.AbstractFunction, + s::MOI.AbstractSet, +) + return MOI.supports_constraint(model.inner, f, s) +end + +function MOI.copy_to( + dest::MOI.Bridges.LazyBridgeOptimizer{MOI.FileFormats.NL.Model}, + src::MOI.ModelLike, +) + model = _CachingModel(dest.model) + # This needs to be `default_copy_to` so that it uses the incremental + # interface. + index_map = MOI.Utilities.default_copy_to(model, src) + MOI.copy_to(model.inner, model.cache) + return index_map +end + end diff --git a/test/FileFormats/NL/NL.jl b/test/FileFormats/NL/NL.jl index c20a25571b..964afde1cb 100644 --- a/test/FileFormats/NL/NL.jl +++ b/test/FileFormats/NL/NL.jl @@ -443,6 +443,156 @@ function test_nlmodel_hs071() return end +function test_nlmodel_hs071_bridged() + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + v = MOI.add_variables(model, 4) + l = [1.1, 1.2, 1.3, 1.4] + u = [5.1, 5.2, 5.3, 5.4] + start = [2.1, 2.2, 2.3, 2.4] + MOI.add_constraint.(model, v, MOI.GreaterThan.(l)) + MOI.add_constraint.(model, v, MOI.LessThan.(u)) + MOI.set.(model, MOI.VariablePrimalStart(), v, start) + lb, ub = [25.0, 40.0], [Inf, 40.0] + evaluator = MOI.Test.HS071(true) + block_data = MOI.NLPBlockData(MOI.NLPBoundsPair.(lb, ub), evaluator, true) + MOI.set(model, MOI.NLPBlock(), block_data) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + n = NL.Model() + @test MOI.supports(n, MOI.NLPBlock()) + @test MOI.supports(n, MOI.ObjectiveSense()) + @test MOI.is_empty(n) + # Replicate what JuMP does in write_to_file! + bridged = MOI.Bridges.full_bridge_optimizer(n, Float64) + MOI.copy_to(bridged, model) + @test !MOI.is_empty(n) + @test n.sense == MOI.MIN_SENSE + @test n.f == NL._NLExpr(MOI.objective_expr(evaluator)) + _test_nlexpr( + n.g[1].expr, + [NL.OPMULT, v[1], NL.OPMULT, v[2], NL.OPMULT, v[3], v[4]], + Dict(v .=> 0.0), + 0.0, + ) + @test n.g[1].lower == 25.0 + @test n.g[1].upper == Inf + @test n.g[1].opcode == 2 + _test_nlexpr( + n.g[2].expr, + [ + NL.OPSUMLIST, + 4, + NL.OPPOW, + v[1], + 2, + NL.OPPOW, + v[2], + 2, + NL.OPPOW, + v[3], + 2, + NL.OPPOW, + v[4], + 2, + ], + Dict(v .=> 0.0), + 0.0, + ) + @test n.g[2].lower == 40.0 + @test n.g[2].upper == 40.0 + @test n.g[2].opcode == 4 + @test length(n.h) == 0 + for i in 1:4 + @test n.x[v[i]].lower == l[i] + @test n.x[v[i]].upper == u[i] + @test n.x[v[i]].type == NL._CONTINUOUS + @test n.x[v[i]].jacobian_count == 2 + @test n.x[v[i]].in_nonlinear_constraint + @test n.x[v[i]].in_nonlinear_objective + @test 0 <= n.x[v[i]].order <= 3 + end + @test length(n.types[1]) == 4 + @test sprint(write, n) == """ + g3 1 1 0 + 4 2 1 0 1 0 + 2 1 + 0 0 + 4 4 4 + 0 0 0 1 + 0 0 0 0 0 + 8 4 + 0 0 + 0 0 0 0 0 + C0 + o2 + v0 + o2 + v1 + o2 + v2 + v3 + C1 + o54 + 4 + o5 + v0 + n2 + o5 + v1 + n2 + o5 + v2 + n2 + o5 + v3 + n2 + O0 0 + o0 + o2 + v0 + o2 + v3 + o54 + 3 + v0 + v1 + v2 + v2 + x4 + 0 2.1 + 1 2.2 + 2 2.3 + 3 2.4 + r + 2 25 + 4 40 + b + 0 1.1 5.1 + 0 1.2 5.2 + 0 1.3 5.3 + 0 1.4 5.4 + k3 + 2 + 4 + 6 + J0 4 + 0 0 + 1 0 + 2 0 + 3 0 + J1 4 + 0 0 + 1 0 + 2 0 + 3 0 + G0 4 + 0 0 + 1 0 + 2 0 + 3 0 + """ + return +end + function test_nlmodel_hs071_linear_obj() model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) v = MOI.add_variables(model, 4)