Skip to content

Commit c755a67

Browse files
authored
[FileFormats.NL] add support for MOI.Complements (#2916)
1 parent 60e2e57 commit c755a67

6 files changed

Lines changed: 472 additions & 7 deletions

File tree

src/FileFormats/NL/NL.jl

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ mutable struct Model <: MOI.ModelLike
146146
MOI.Utilities.UniversalFallback{MOI.Utilities.Model{Float64}},
147147
}
148148
use_nlp_block::Bool
149+
complementarity_constraints::Vector{Vector{Int}}
149150

150151
function Model(; use_nlp_block::Bool = true)
151152
return new(
@@ -160,6 +161,7 @@ mutable struct Model <: MOI.ModelLike
160161
MOI.VariableIndex[],
161162
nothing,
162163
use_nlp_block,
164+
Vector{Int}[],
163165
)
164166
end
165167
end
@@ -185,6 +187,7 @@ function MOI.empty!(model::Model)
185187
end
186188
empty!(model.order)
187189
model.model = nothing
190+
empty!(model.complementarity_constraints)
188191
return
189192
end
190193

@@ -222,6 +225,21 @@ function MOI.supports_constraint(
222225
return true
223226
end
224227

228+
function MOI.supports_constraint(
229+
::Model,
230+
::Type{F},
231+
::Type{MOI.Complements},
232+
) where {
233+
F<:Union{
234+
MOI.VectorOfVariables,
235+
MOI.VectorAffineFunction{Float64},
236+
MOI.VectorQuadraticFunction{Float64},
237+
MOI.VectorNonlinearFunction,
238+
},
239+
}
240+
return true
241+
end
242+
225243
MOI.supports(::Model, ::MOI.ObjectiveSense) = true
226244
MOI.supports(::Model, ::MOI.ObjectiveFunction{<:_SCALAR_FUNCTIONS}) = true
227245

@@ -493,6 +511,48 @@ function _process_constraint(
493511
return
494512
end
495513

514+
_to_x(f) = convert(MOI.VariableIndex, f)
515+
516+
function _to_x(f::MOI.ScalarNonlinearFunction)
517+
# Hacky way to ensure that f is a standalone variable
518+
@assert f isa MOI.ScalarNonlinearFunction
519+
@assert f.head == :+ && length(f.args) == 1
520+
@assert f.args[1] isa MOI.VariableIndex
521+
return return f.args[1]
522+
end
523+
524+
function _process_constraint(
525+
dest::Model,
526+
model,
527+
::Type{F},
528+
::Type{S},
529+
mapping,
530+
) where {F,S<:MOI.Complements}
531+
ci_src = MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
532+
for ci in ci_src
533+
f_vec = MOI.get(model, MOI.ConstraintFunction(), ci)
534+
f_scalars = MOI.Utilities.scalarize(f_vec)
535+
n = div(MOI.output_dimension(f_vec), 2)
536+
rows = Int[]
537+
for i in 1:n
538+
fi, xi = f_scalars[i], _to_x(f_scalars[i+n])
539+
con = _NLConstraint(Float64(xi.value), Inf, 5, _NLExpr(fi))
540+
if con.expr.is_linear
541+
push!(dest.h, con)
542+
push!(rows, -length(dest.h))
543+
else
544+
push!(dest.g, con)
545+
push!(rows, length(dest.g))
546+
end
547+
end
548+
push!(dest.complementarity_constraints, rows)
549+
mapping[ci] =
550+
MOI.ConstraintIndex{F,S}(length(dest.complementarity_constraints))
551+
end
552+
MOI.Utilities.pass_attributes(dest, model, mapping, ci_src)
553+
return
554+
end
555+
496556
function _str(x::Float64)
497557
if isinteger(x) && (typemin(Int) <= x <= typemax(Int))
498558
return string(round(Int, x))
@@ -571,8 +631,19 @@ function Base.write(io::IO, model::Model)
571631
# Line 3: nonlinear constraints, objectives
572632
# Notes:
573633
# * We assume there is always one objective, even if it is just `min 0`.
634+
# * `Writing .nl Files` lies: there are four extra integers here
635+
# * Number of linear complementarity constraints
636+
# * Number of nonlinear complementarity constraints
637+
# * nd: I have no idea
638+
# * nzlb: I have no idea
574639
n_nlcon = length(model.g)
575-
println(io, " ", n_nlcon, " ", 1)
640+
ccon_lin = sum(c.opcode == 5 for c in model.h; init = 0)
641+
ccon_nl = sum(c.opcode == 5 for c in model.g; init = 0)
642+
if ccon_lin + ccon_nl > 0
643+
println(io, " ", n_nlcon, " 1 ", ccon_lin, " ", ccon_nl, " 0 0")
644+
else
645+
println(io, " ", n_nlcon, " ", 1)
646+
end
576647

577648
# Line 4: network constraints: nonlinear, linear
578649
# Notes:
@@ -694,9 +765,15 @@ function Base.write(io::IO, model::Model)
694765
println(io, " ", _str(g.lower))
695766
elseif g.opcode == 3
696767
println(io)
697-
else
698-
@assert g.opcode == 4
768+
elseif g.opcode == 4
699769
println(io, " ", _str(g.lower))
770+
else
771+
@assert g.opcode == 5
772+
@assert !isfinite(g.upper)
773+
x = MOI.VariableIndex(g.lower)
774+
v = model.x[x]
775+
k = (-Inf < v.lower) + 2 * (v.upper < Inf)
776+
println(io, " ", k, " ", v.order + 1)
700777
end
701778
end
702779
# Linear constraints
@@ -710,9 +787,15 @@ function Base.write(io::IO, model::Model)
710787
println(io, " ", _str(h.lower))
711788
elseif h.opcode == 3
712789
println(io)
713-
else
714-
@assert h.opcode == 4
790+
elseif h.opcode == 4
715791
println(io, " ", _str(h.lower))
792+
else
793+
@assert h.opcode == 5
794+
@assert !isfinite(h.upper)
795+
x = MOI.VariableIndex(h.lower)
796+
v = model.x[x]
797+
k = (-Inf < v.lower) + 2 * (v.upper < Inf)
798+
println(io, " ", k, " ", v.order + 1)
716799
end
717800
end
718801
end

src/FileFormats/NL/read.jl

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ mutable struct _CacheModel
1616
constraint_upper::Vector{Float64}
1717
objective::Expr
1818
sense::MOI.OptimizationSense
19+
complements_map::Dict{Int,Int}
20+
1921
function _CacheModel()
2022
return new(
2123
false,
@@ -29,6 +31,7 @@ mutable struct _CacheModel
2931
Float64[],
3032
:(),
3133
MOI.FEASIBILITY_SENSE,
34+
Dict{Int,Int}(),
3235
)
3336
end
3437
end
@@ -208,6 +211,7 @@ function _to_model(data::_CacheModel; use_nlp_block::Bool)
208211
MOI.set(model, MOI.ObjectiveSense(), data.sense)
209212
end
210213
if use_nlp_block
214+
@assert isempty(data.complements_map)
211215
nlp = MOI.Nonlinear.Model()
212216
if data.objective != :()
213217
MOI.Nonlinear.set_objective(nlp, data.objective)
@@ -234,6 +238,16 @@ function _to_model(data::_CacheModel; use_nlp_block::Bool)
234238
MOI.set(model, MOI.ObjectiveFunction{typeof(obj)}(), obj)
235239
end
236240
for (i, expr) in enumerate(data.constraints)
241+
if haskey(data.complements_map, i)
242+
g = MOI.Utilities.operate(
243+
vcat,
244+
Float64,
245+
_expr_to_function(expr),
246+
x[data.complements_map[i]],
247+
)
248+
MOI.add_constraint(model, g, MOI.Complements(2))
249+
continue
250+
end
237251
lb, ub = data.constraint_lower[i], data.constraint_upper[i]
238252
f = _expr_to_function(expr)
239253
if lb == ub
@@ -551,11 +565,15 @@ function _parse_section(io::IO, ::Val{'r'}, model::_CacheModel)
551565
model.constraint_lower[i] = _next(Float64, io, model)
552566
elseif type == Cchar('3')
553567
# Free constraint
554-
else
555-
@assert type == Cchar('4')
568+
elseif type == Cchar('4')
556569
value = _next(Float64, io, model)
557570
model.constraint_lower[i] = value
558571
model.constraint_upper[i] = value
572+
else
573+
@assert type == Cchar('5')
574+
_ = _next(Int, io, model) # k
575+
j = _next(Int, io, model) # variable i-1
576+
push!(model.complements_map, i => j)
559577
end
560578
_read_til_newline(io, model)
561579
end

test/FileFormats/NL/NL.jl

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,144 @@ function test_unsupported_objectives()
13941394
return
13951395
end
13961396

1397+
function test_write_complements_VectorOfVariables()
1398+
for (set, k, b) in (
1399+
(MOI.GreaterThan(0.0), 1, "2 0"),
1400+
(MOI.LessThan(1.0), 2, "1 1"),
1401+
(MOI.EqualTo(1.0), 3, "4 1"),
1402+
(MOI.Interval(0.0, 1.0), 3, "0 0 1"),
1403+
)
1404+
src = MOI.Utilities.Model{Float64}()
1405+
x = MOI.add_variable(src)
1406+
y, _ = MOI.add_constrained_variable(src, MOI.Interval(0.0, 1.0))
1407+
MOI.add_constraint(src, x, set)
1408+
MOI.add_constraint(
1409+
src,
1410+
MOI.Utilities.vectorize([y, x]),
1411+
MOI.Complements(2),
1412+
)
1413+
dest = NL.Model()
1414+
MOI.copy_to(dest, src)
1415+
@test sprint(write, dest) == """
1416+
g3 1 1 0
1417+
2 1 1 0 0 0
1418+
0 1 1 0 0 0
1419+
0 0
1420+
0 0 0
1421+
0 0 0 1
1422+
0 0 0 0 0
1423+
1 0
1424+
0 0
1425+
0 0 0 0 0
1426+
C0
1427+
n0
1428+
O0 0
1429+
n0
1430+
x2
1431+
0 0
1432+
1 0
1433+
r
1434+
5 $k 1
1435+
b
1436+
$b
1437+
0 0 1
1438+
k1
1439+
0
1440+
J0 1
1441+
1 1
1442+
"""
1443+
end
1444+
return
1445+
end
1446+
1447+
function test_write_complements_VectorAffineFunction()
1448+
for (set, k, b) in (
1449+
(MOI.GreaterThan(0.0), 1, "2 0"),
1450+
(MOI.LessThan(1.0), 2, "1 1"),
1451+
(MOI.EqualTo(1.0), 3, "4 1"),
1452+
(MOI.Interval(0.0, 1.0), 3, "0 0 1"),
1453+
)
1454+
src = MOI.Utilities.Model{Float64}()
1455+
x = MOI.add_variable(src)
1456+
MOI.add_constraint(src, x, set)
1457+
MOI.add_constraint(
1458+
src,
1459+
MOI.Utilities.vectorize([1.0 - x, x]),
1460+
MOI.Complements(2),
1461+
)
1462+
dest = NL.Model()
1463+
MOI.copy_to(dest, src)
1464+
@test sprint(write, dest) == """
1465+
g3 1 1 0
1466+
1 1 1 0 0 0
1467+
0 1 1 0 0 0
1468+
0 0
1469+
0 0 0
1470+
0 0 0 1
1471+
0 0 0 0 0
1472+
1 0
1473+
0 0
1474+
0 0 0 0 0
1475+
C0
1476+
n1
1477+
O0 0
1478+
n0
1479+
x1
1480+
0 0
1481+
r
1482+
5 $k 1
1483+
b
1484+
$b
1485+
k0
1486+
J0 1
1487+
0 -1
1488+
"""
1489+
end
1490+
return
1491+
end
1492+
1493+
function test_write_complements_VectorNonlinearFunction()
1494+
src = MOI.Utilities.Model{Float64}()
1495+
x, _ = MOI.add_constrained_variable(src, MOI.Interval(0.0, 1.0))
1496+
MOI.add_constraint(
1497+
src,
1498+
MOI.VectorNonlinearFunction([
1499+
MOI.ScalarNonlinearFunction(:sin, Any[x]),
1500+
MOI.ScalarNonlinearFunction(:+, Any[x]),
1501+
]),
1502+
MOI.Complements(2),
1503+
)
1504+
dest = NL.Model()
1505+
MOI.copy_to(dest, src)
1506+
@test sprint(write, dest) == """
1507+
g3 1 1 0
1508+
1 1 1 0 0 0
1509+
1 1 0 1 0 0
1510+
0 0
1511+
1 0 0
1512+
0 0 0 1
1513+
0 0 0 0 0
1514+
1 0
1515+
0 0
1516+
0 0 0 0 0
1517+
C0
1518+
o41
1519+
v0
1520+
O0 0
1521+
n0
1522+
x1
1523+
0 0
1524+
r
1525+
5 3 1
1526+
b
1527+
0 0 1
1528+
k0
1529+
J0 1
1530+
0 0
1531+
"""
1532+
return
1533+
end
1534+
13971535
end
13981536

13991537
TestNLModel.runtests()

0 commit comments

Comments
 (0)