diff --git a/src/FileFormats/NL/NL.jl b/src/FileFormats/NL/NL.jl index d59f1bdedb..149d40f9a8 100644 --- a/src/FileFormats/NL/NL.jl +++ b/src/FileFormats/NL/NL.jl @@ -146,6 +146,7 @@ mutable struct Model <: MOI.ModelLike MOI.Utilities.UniversalFallback{MOI.Utilities.Model{Float64}}, } use_nlp_block::Bool + complementarity_constraints::Vector{Vector{Int}} function Model(; use_nlp_block::Bool = true) return new( @@ -160,6 +161,7 @@ mutable struct Model <: MOI.ModelLike MOI.VariableIndex[], nothing, use_nlp_block, + Vector{Int}[], ) end end @@ -185,6 +187,7 @@ function MOI.empty!(model::Model) end empty!(model.order) model.model = nothing + empty!(model.complementarity_constraints) return end @@ -222,6 +225,21 @@ function MOI.supports_constraint( return true end +function MOI.supports_constraint( + ::Model, + ::Type{F}, + ::Type{MOI.Complements}, +) where { + F<:Union{ + MOI.VectorOfVariables, + MOI.VectorAffineFunction{Float64}, + MOI.VectorQuadraticFunction{Float64}, + MOI.VectorNonlinearFunction, + }, +} + return true +end + MOI.supports(::Model, ::MOI.ObjectiveSense) = true MOI.supports(::Model, ::MOI.ObjectiveFunction{<:_SCALAR_FUNCTIONS}) = true @@ -493,6 +511,48 @@ function _process_constraint( return end +_to_x(f) = convert(MOI.VariableIndex, f) + +function _to_x(f::MOI.ScalarNonlinearFunction) + # Hacky way to ensure that f is a standalone variable + @assert f isa MOI.ScalarNonlinearFunction + @assert f.head == :+ && length(f.args) == 1 + @assert f.args[1] isa MOI.VariableIndex + return return f.args[1] +end + +function _process_constraint( + dest::Model, + model, + ::Type{F}, + ::Type{S}, + mapping, +) where {F,S<:MOI.Complements} + ci_src = MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + for ci in ci_src + f_vec = MOI.get(model, MOI.ConstraintFunction(), ci) + f_scalars = MOI.Utilities.scalarize(f_vec) + n = div(MOI.output_dimension(f_vec), 2) + rows = Int[] + for i in 1:n + fi, xi = f_scalars[i], _to_x(f_scalars[i+n]) + con = _NLConstraint(Float64(xi.value), Inf, 5, _NLExpr(fi)) + if con.expr.is_linear + push!(dest.h, con) + push!(rows, -length(dest.h)) + else + push!(dest.g, con) + push!(rows, length(dest.g)) + end + end + push!(dest.complementarity_constraints, rows) + mapping[ci] = + MOI.ConstraintIndex{F,S}(length(dest.complementarity_constraints)) + end + MOI.Utilities.pass_attributes(dest, model, mapping, ci_src) + return +end + function _str(x::Float64) if isinteger(x) && (typemin(Int) <= x <= typemax(Int)) return string(round(Int, x)) @@ -571,8 +631,19 @@ function Base.write(io::IO, model::Model) # Line 3: nonlinear constraints, objectives # Notes: # * We assume there is always one objective, even if it is just `min 0`. + # * `Writing .nl Files` lies: there are four extra integers here + # * Number of linear complementarity constraints + # * Number of nonlinear complementarity constraints + # * nd: I have no idea + # * nzlb: I have no idea n_nlcon = length(model.g) - println(io, " ", n_nlcon, " ", 1) + ccon_lin = sum(c.opcode == 5 for c in model.h; init = 0) + ccon_nl = sum(c.opcode == 5 for c in model.g; init = 0) + if ccon_lin + ccon_nl > 0 + println(io, " ", n_nlcon, " 1 ", ccon_lin, " ", ccon_nl, " 0 0") + else + println(io, " ", n_nlcon, " ", 1) + end # Line 4: network constraints: nonlinear, linear # Notes: @@ -694,9 +765,15 @@ function Base.write(io::IO, model::Model) println(io, " ", _str(g.lower)) elseif g.opcode == 3 println(io) - else - @assert g.opcode == 4 + elseif g.opcode == 4 println(io, " ", _str(g.lower)) + else + @assert g.opcode == 5 + @assert !isfinite(g.upper) + x = MOI.VariableIndex(g.lower) + v = model.x[x] + k = (-Inf < v.lower) + 2 * (v.upper < Inf) + println(io, " ", k, " ", v.order + 1) end end # Linear constraints @@ -710,9 +787,15 @@ function Base.write(io::IO, model::Model) println(io, " ", _str(h.lower)) elseif h.opcode == 3 println(io) - else - @assert h.opcode == 4 + elseif h.opcode == 4 println(io, " ", _str(h.lower)) + else + @assert h.opcode == 5 + @assert !isfinite(h.upper) + x = MOI.VariableIndex(h.lower) + v = model.x[x] + k = (-Inf < v.lower) + 2 * (v.upper < Inf) + println(io, " ", k, " ", v.order + 1) end end end diff --git a/src/FileFormats/NL/read.jl b/src/FileFormats/NL/read.jl index 28e718a4ff..8d500b5322 100644 --- a/src/FileFormats/NL/read.jl +++ b/src/FileFormats/NL/read.jl @@ -16,6 +16,8 @@ mutable struct _CacheModel constraint_upper::Vector{Float64} objective::Expr sense::MOI.OptimizationSense + complements_map::Dict{Int,Int} + function _CacheModel() return new( false, @@ -29,6 +31,7 @@ mutable struct _CacheModel Float64[], :(), MOI.FEASIBILITY_SENSE, + Dict{Int,Int}(), ) end end @@ -208,6 +211,7 @@ function _to_model(data::_CacheModel; use_nlp_block::Bool) MOI.set(model, MOI.ObjectiveSense(), data.sense) end if use_nlp_block + @assert isempty(data.complements_map) nlp = MOI.Nonlinear.Model() if data.objective != :() MOI.Nonlinear.set_objective(nlp, data.objective) @@ -234,6 +238,16 @@ function _to_model(data::_CacheModel; use_nlp_block::Bool) MOI.set(model, MOI.ObjectiveFunction{typeof(obj)}(), obj) end for (i, expr) in enumerate(data.constraints) + if haskey(data.complements_map, i) + g = MOI.Utilities.operate( + vcat, + Float64, + _expr_to_function(expr), + x[data.complements_map[i]], + ) + MOI.add_constraint(model, g, MOI.Complements(2)) + continue + end lb, ub = data.constraint_lower[i], data.constraint_upper[i] f = _expr_to_function(expr) if lb == ub @@ -551,11 +565,15 @@ function _parse_section(io::IO, ::Val{'r'}, model::_CacheModel) model.constraint_lower[i] = _next(Float64, io, model) elseif type == Cchar('3') # Free constraint - else - @assert type == Cchar('4') + elseif type == Cchar('4') value = _next(Float64, io, model) model.constraint_lower[i] = value model.constraint_upper[i] = value + else + @assert type == Cchar('5') + _ = _next(Int, io, model) # k + j = _next(Int, io, model) # variable i-1 + push!(model.complements_map, i => j) end _read_til_newline(io, model) end diff --git a/test/FileFormats/NL/NL.jl b/test/FileFormats/NL/NL.jl index 8d2123e0ab..7bf70522b2 100644 --- a/test/FileFormats/NL/NL.jl +++ b/test/FileFormats/NL/NL.jl @@ -1394,6 +1394,144 @@ function test_unsupported_objectives() return end +function test_write_complements_VectorOfVariables() + for (set, k, b) in ( + (MOI.GreaterThan(0.0), 1, "2 0"), + (MOI.LessThan(1.0), 2, "1 1"), + (MOI.EqualTo(1.0), 3, "4 1"), + (MOI.Interval(0.0, 1.0), 3, "0 0 1"), + ) + src = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(src) + y, _ = MOI.add_constrained_variable(src, MOI.Interval(0.0, 1.0)) + MOI.add_constraint(src, x, set) + MOI.add_constraint( + src, + MOI.Utilities.vectorize([y, x]), + MOI.Complements(2), + ) + dest = NL.Model() + MOI.copy_to(dest, src) + @test sprint(write, dest) == """ + g3 1 1 0 + 2 1 1 0 0 0 + 0 1 1 0 0 0 + 0 0 + 0 0 0 + 0 0 0 1 + 0 0 0 0 0 + 1 0 + 0 0 + 0 0 0 0 0 + C0 + n0 + O0 0 + n0 + x2 + 0 0 + 1 0 + r + 5 $k 1 + b + $b + 0 0 1 + k1 + 0 + J0 1 + 1 1 + """ + end + return +end + +function test_write_complements_VectorAffineFunction() + for (set, k, b) in ( + (MOI.GreaterThan(0.0), 1, "2 0"), + (MOI.LessThan(1.0), 2, "1 1"), + (MOI.EqualTo(1.0), 3, "4 1"), + (MOI.Interval(0.0, 1.0), 3, "0 0 1"), + ) + src = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(src) + MOI.add_constraint(src, x, set) + MOI.add_constraint( + src, + MOI.Utilities.vectorize([1.0 - x, x]), + MOI.Complements(2), + ) + dest = NL.Model() + MOI.copy_to(dest, src) + @test sprint(write, dest) == """ + g3 1 1 0 + 1 1 1 0 0 0 + 0 1 1 0 0 0 + 0 0 + 0 0 0 + 0 0 0 1 + 0 0 0 0 0 + 1 0 + 0 0 + 0 0 0 0 0 + C0 + n1 + O0 0 + n0 + x1 + 0 0 + r + 5 $k 1 + b + $b + k0 + J0 1 + 0 -1 + """ + end + return +end + +function test_write_complements_VectorNonlinearFunction() + src = MOI.Utilities.Model{Float64}() + x, _ = MOI.add_constrained_variable(src, MOI.Interval(0.0, 1.0)) + MOI.add_constraint( + src, + MOI.VectorNonlinearFunction([ + MOI.ScalarNonlinearFunction(:sin, Any[x]), + MOI.ScalarNonlinearFunction(:+, Any[x]), + ]), + MOI.Complements(2), + ) + dest = NL.Model() + MOI.copy_to(dest, src) + @test sprint(write, dest) == """ + g3 1 1 0 + 1 1 1 0 0 0 + 1 1 0 1 0 0 + 0 0 + 1 0 0 + 0 0 0 1 + 0 0 0 0 0 + 1 0 + 0 0 + 0 0 0 0 0 + C0 + o41 + v0 + O0 0 + n0 + x1 + 0 0 + r + 5 3 1 + b + 0 0 1 + k0 + J0 1 + 0 0 + """ + return +end + end TestNLModel.runtests() diff --git a/test/FileFormats/NL/data/josephy.nl b/test/FileFormats/NL/data/josephy.nl new file mode 100644 index 0000000000..90cbc3d839 --- /dev/null +++ b/test/FileFormats/NL/data/josephy.nl @@ -0,0 +1,104 @@ +g3 1 1 0 # problem josephy + 4 4 0 0 0 # vars, constraints, objectives, ranges, eqns + 4 0 0 4 0 0 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 # network constraints: nonlinear, linear + 2 0 0 # nonlinear vars in constraints, objectives, both + 0 0 0 1 # linear network variables; functions; arith, flags + 0 0 0 0 0 # discrete variables: binary, integer, nonlinear (b,c,o) + 16 0 # nonzeros in Jacobian, gradients + 0 0 # max name lengths: constraints, variables + 0 0 0 0 0 # common exprs: b,c,o,c1,o1 +b +2 0 +2 0 +2 0 +2 0 +r +5 1 1 +5 1 2 +5 1 3 +5 1 4 +C0 +o54 +4 +o2 +o2 +n3 +v0 +v0 +o2 +o2 +n2 +v0 +v1 +o2 +o2 +n2 +v1 +v1 +n-6 +C1 +o54 +3 +o2 +o2 +n2 +v0 +v0 +o2 +v1 +v1 +n-2 +C2 +o54 +4 +o2 +o2 +n3 +v0 +v0 +o2 +v0 +v1 +o2 +o2 +n2 +v1 +v1 +n-1 +C3 +o54 +3 +o2 +v0 +v0 +o2 +o2 +n3 +v1 +v1 +n-3 +k3 +4 +8 +12 +J0 4 +0 0 +1 0 +2 1 +3 3 +J1 4 +0 1 +1 0 +2 3 +3 2 +J2 4 +0 0 +1 0 +2 2 +3 3 +J3 4 +0 0 +1 0 +2 2 +3 3 diff --git a/test/FileFormats/NL/data/sample.nl b/test/FileFormats/NL/data/sample.nl new file mode 100644 index 0000000000..436774ca13 --- /dev/null +++ b/test/FileFormats/NL/data/sample.nl @@ -0,0 +1,80 @@ +g3 0 1 0 # problem sample + 8 8 0 0 4 # vars, constraints, objectives, ranges, eqns + 0 0 4 0 0 4 # nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 # network constraints: nonlinear, linear + 0 0 0 # nonlinear vars in constraints, objectives, both + 0 0 0 1 # linear network variables; functions; arith, flags + 0 0 0 0 0 # discrete variables: binary, integer, nonlinear (b,c,o) + 20 0 # nonzeros in Jacobian, gradients + 0 0 # max name lengths: constraints, variables + 0 0 0 0 0 # common exprs: b,c,o,c1,o1 +b +0 0 10 +0 0 10 +0 0 10 +0 0 10 +2 -2 +2 -2 +2 2 +2 6 +r +4 0 +5 1 5 +4 0 +5 1 6 +4 0 +5 1 7 +4 0 +5 1 8 +C0 +n0 +C1 +n0 +C2 +n0 +C3 +n0 +C4 +n0 +C5 +n0 +C6 +n0 +C7 +n0 +k7 +3 +6 +11 +16 +17 +18 +19 +J0 3 +2 -1 +3 -1 +4 -1 +J1 1 +0 1 +J2 3 +2 1 +3 -2 +5 -1 +J3 1 +1 1 +J4 5 +0 1 +1 -1 +2 2 +3 -2 +6 -1 +J5 1 +2 1 +J6 5 +0 1 +1 2 +2 -2 +3 4 +7 -1 +J7 1 +3 1 diff --git a/test/FileFormats/NL/read.jl b/test/FileFormats/NL/read.jl index c52c59597e..122b850211 100644 --- a/test/FileFormats/NL/read.jl +++ b/test/FileFormats/NL/read.jl @@ -1090,6 +1090,48 @@ function test_try_scalar_affine_function() return end +function test_complements_sample() + model = NL.Model(; use_nlp_block = false) + open(joinpath(@__DIR__, "data", "sample.nl"), "r") do io + return read!(io, model) + end + F, S = MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64} + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 4 + x = MOI.get(model, MOI.ListOfVariableIndices()) + @test length(x) == 8 + F, S = MOI.VectorAffineFunction{Float64}, MOI.Complements + c = MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + @test length(c) == 4 + for i in 1:4 + @test isapprox( + MOI.get(model, MOI.ConstraintFunction(), c[i]), + MOI.Utilities.vectorize([1.0 * x[i], 1.0 * x[i+4]]), + ) + end + return +end + +function test_complements_josephy() + model = NL.Model(; use_nlp_block = false) + open(joinpath(@__DIR__, "data", "josephy.nl"), "r") do io + return read!(io, model) + end + x = MOI.get(model, MOI.ListOfVariableIndices()) + @test length(x) == 4 + F, S = MOI.VectorNonlinearFunction, MOI.Complements + c = MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + @test length(c) == 4 + for i in 1:4 + f = MOI.get(model, MOI.ConstraintFunction(), c[i]) + @test isapprox(f.rows[2], MOI.ScalarNonlinearFunction(:+, Any[x[i]])) + end + ret = MOI.get(model, MOI.ListOfConstraintTypesPresent()) + @test length(ret) == 2 + @test (MOI.VariableIndex, MOI.GreaterThan{Float64}) in ret + @test (F, S) in ret + return +end + end TestNonlinearRead.runtests()