Skip to content

Commit f89036a

Browse files
authored
[FileFormats.CBF] write out EXP and EXP* as variable cones (#2482)
1 parent f8f1f0a commit f89036a

File tree

5 files changed

+156
-47
lines changed

5 files changed

+156
-47
lines changed

src/FileFormats/CBF/read.jl

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,9 @@ function _read_VAR(io::IO, model::Model, data::_CBFReadData)
149149
# Free cones (no constraint).
150150
append!(data.scalar_vars, MOI.add_variables(model, cone_dim))
151151
elseif cone_str == "EXP" || cone_str == "EXP*"
152-
# The convention in CBF is the reverse of MOI, so we cannot use
153-
# add_constrained_variables.
154-
x = MOI.add_variables(model, 3)
155-
append!(data.scalar_vars, x)
156-
MOI.add_constraint(model, MOI.VectorOfVariables(reverse(x)), set)
152+
# The convention in CBF is the reverse of MOI
153+
x, _ = MOI.add_constrained_variables(model, set)
154+
append!(data.scalar_vars, reverse(x))
157155
else
158156
x, _ = MOI.add_constrained_variables(model, set)
159157
append!(data.scalar_vars, x)

src/FileFormats/CBF/write.jl

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ mutable struct _CBFDataStructure
2121
dcoord::Vector{Tuple{Int,Int,Int,Float64}}
2222
variables_with_domain::Set{MOI.VariableIndex}
2323
variable_cones::Vector{Tuple{Vector{MOI.VariableIndex},String}}
24+
# This is the identity mapping, except for EXP and EXP* cones,
25+
# in which (u, v, w) in MOI are swapped to (w, v, u) in CBF.
26+
scalar_variables::Vector{Int}
2427

2528
function _CBFDataStructure()
2629
return new(
@@ -35,6 +38,7 @@ mutable struct _CBFDataStructure
3538
Tuple{Int,Int,Int,Float64}[],
3639
Set{MOI.VariableIndex}(),
3740
Tuple{Vector{MOI.VariableIndex},String}[],
41+
Int[],
3842
)
3943
end
4044
end
@@ -161,26 +165,6 @@ function _add_cones(
161165
return
162166
end
163167

164-
function _add_cones(
165-
data::_CBFDataStructure,
166-
model::Model,
167-
::Type{F},
168-
::Type{S},
169-
) where {
170-
F<:MOI.VectorOfVariables,
171-
S<:Union{MOI.ExponentialCone,MOI.DualExponentialCone},
172-
}
173-
# The Exponential cone in MOI and CBF are reversed. Instead of dealing with
174-
# this complexity, just write them out as `Ax + b in K` constraints.
175-
# TODO(odow): we should support this at some point. See #2478
176-
for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
177-
f = MOI.get(model, MOI.ConstraintFunction(), ci)
178-
_add_function(data, f, S)
179-
push!(data.cones, (_cone_string(data, S), 3))
180-
end
181-
return
182-
end
183-
184168
function _add_cones(
185169
data::_CBFDataStructure,
186170
model::Model,
@@ -310,6 +294,7 @@ end
310294

311295
function _write_VAR(io::IO, model::Model, data)
312296
num_var = MOI.get(model, MOI.NumberOfVariables())
297+
append!(data.scalar_variables, collect(1:num_var))
313298
cones = Tuple{String,Int}[]
314299
current_variable = 0
315300
for (f, str) in sort!(data.variable_cones; by = x -> first(x[1]).value)
@@ -325,14 +310,22 @@ function _write_VAR(io::IO, model::Model, data)
325310
end
326311
println(io, "VAR")
327312
println(io, num_var, " ", length(cones))
313+
offset = 1
328314
for (K, n) in cones
329315
println(io, K, " ", n)
316+
if K == "EXP" || K == "EXP*"
317+
# The ordering for EXP and EXP* is reversed between MOI and CBF
318+
tmp = data.scalar_variables[offset]
319+
data.scalar_variables[offset] = data.scalar_variables[offset+2]
320+
data.scalar_variables[offset+2] = tmp
321+
end
322+
offset += n
330323
end
331324
println(io)
332325
return
333326
end
334327

335-
function _write_INT(io::IO, model::Model)
328+
function _write_INT(io::IO, model::Model, data)
336329
cons = MOI.get(
337330
model,
338331
MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}(),
@@ -344,7 +337,7 @@ function _write_INT(io::IO, model::Model)
344337
println(io, length(cons))
345338
for ci in cons
346339
f = MOI.get(model, MOI.ConstraintFunction(), ci)
347-
println(io, f.value - 1)
340+
println(io, data.scalar_variables[f.value] - 1)
348341
end
349342
println(io)
350343
return
@@ -362,15 +355,16 @@ function _write_PSDCON(io::IO, data::_CBFDataStructure)
362355
return
363356
end
364357

365-
function _write_OBJACOORD(io::IO, model::Model)
358+
function _write_OBJACOORD(io::IO, model::Model, data)
366359
f = MOI.get(
367360
model,
368361
MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
369362
)
370363
if !isempty(f.terms)
371364
println(io, "OBJACOORD\n", length(f.terms))
372365
for t in f.terms
373-
println(io, t.variable.value - 1, " ", t.coefficient)
366+
column = data.scalar_variables[t.variable.value] - 1
367+
println(io, column, " ", t.coefficient)
374368
end
375369
println(io)
376370
end
@@ -402,7 +396,8 @@ function _write_ACOORD(io::IO, data::_CBFDataStructure)
402396
end
403397
println(io, "ACOORD\n", length(data.acoord))
404398
for (row, var, coef) in data.acoord
405-
println(io, row - 1, " ", var - 1, " ", coef)
399+
column = data.scalar_variables[var] - 1
400+
println(io, row - 1, " ", column, " ", coef)
406401
end
407402
println(io)
408403
return
@@ -430,7 +425,7 @@ function _write_HCOORD(io::IO, data::_CBFDataStructure)
430425
io,
431426
psd_idx - 1,
432427
" ",
433-
var - 1,
428+
data.scalar_variables[var] - 1,
434429
" ",
435430
i - 1,
436431
" ",
@@ -489,15 +484,15 @@ function Base.write(io::IO, model::Model)
489484
_write_OBJSENSE(io, model)
490485
# _write_PSDVAR
491486
_write_VAR(io, model, data)
492-
_write_INT(io, model)
487+
_write_INT(io, model, data)
493488
_write_PSDCON(io, data)
494489
_write_CON(io, data)
495490

496491
###
497492
### Problem data
498493
###
499494
# _write_OBJFCOORD
500-
constant = _write_OBJACOORD(io, model)
495+
constant = _write_OBJACOORD(io, model, data)
501496
_write_OBJBCOORD(io, constant)
502497
# _write_FCOORD
503498
_write_ACOORD(io, data)

test/FileFormats/CBF/CBF.jl

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -412,15 +412,13 @@ const _EXAMPLE_MODELS = [
412412
(
413413
"example_C.cbf",
414414
"""
415-
variables: a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p
416-
maxobjective: a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p + -1
415+
variables: a, b, c, d, e, f, g, h, i, j
416+
maxobjective: a + b + c + d + e + f + g + h + i + j + -1
417417
c1: [b] in Zeros(1)
418418
c2: [c] in Nonnegatives(1)
419419
c3: [d] in Nonpositives(1)
420420
c4: [e, f, g] in SecondOrderCone(3)
421421
c5: [h, i, j] in RotatedSecondOrderCone(3)
422-
c6: [m, l, k] in ExponentialCone()
423-
c7: [p, o, n] in DualExponentialCone()
424422
""",
425423
),
426424
(
@@ -500,6 +498,132 @@ function test_write_variable_cones()
500498
return
501499
end
502500

501+
function test_roundtrip_ExponentialCone()
502+
model = CBF.Model()
503+
x, _ = MOI.add_constrained_variables(model, MOI.ExponentialCone())
504+
f = 1.0 * x[1] + 2.0 * x[2] + 3.0 * x[3]
505+
MOI.add_constraint(model, x[1], MOI.Integer())
506+
MOI.add_constraint(
507+
model,
508+
MOI.VectorOfVariables([x[1], x[2], x[1]]),
509+
MOI.PositiveSemidefiniteConeTriangle(2),
510+
)
511+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
512+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
513+
g = MOI.Utilities.operate(vcat, Float64, f)
514+
MOI.add_constraint(model, g, MOI.Zeros(1))
515+
io = IOBuffer()
516+
write(io, model)
517+
seekstart(io)
518+
@test read(io, String) == """
519+
VER
520+
3
521+
522+
OBJSENSE
523+
MIN
524+
525+
VAR
526+
3 1
527+
EXP 3
528+
529+
INT
530+
1
531+
2
532+
533+
PSDCON
534+
1
535+
2
536+
537+
CON
538+
1 1
539+
L= 1
540+
541+
OBJACOORD
542+
3
543+
2 1.0
544+
1 2.0
545+
0 3.0
546+
547+
ACOORD
548+
3
549+
0 2 1.0
550+
0 1 2.0
551+
0 0 3.0
552+
553+
BCOORD
554+
1
555+
0 0.0
556+
557+
HCOORD
558+
3
559+
0 2 0 0 1.0
560+
0 1 1 0 1.0
561+
0 2 1 1 1.0
562+
563+
"""
564+
seekstart(io)
565+
model2 = CBF.Model()
566+
read!(io, model2)
567+
y = MOI.get(model2, MOI.ListOfVariableIndices())
568+
obj_y = MOI.get(model2, MOI.ObjectiveFunction{typeof(f)}())
569+
@test (obj_y, 1.0 * y[1] + 2.0 * y[2] + 3.0 * y[3])
570+
ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Integer}.(1:3)
571+
@test MOI.is_valid.(model2, ci) == [true, false, false]
572+
return
573+
end
574+
575+
function test_roundtrip_DualExponentialCone()
576+
model = CBF.Model()
577+
x, _ = MOI.add_constrained_variables(model, MOI.DualExponentialCone())
578+
f = 1.0 * x[1] + 2.0 * x[2] + 3.0 * x[3]
579+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
580+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
581+
g = MOI.Utilities.operate(vcat, Float64, f)
582+
MOI.add_constraint(model, g, MOI.Zeros(1))
583+
io = IOBuffer()
584+
write(io, model)
585+
seekstart(io)
586+
@test read(io, String) == """
587+
VER
588+
3
589+
590+
OBJSENSE
591+
MIN
592+
593+
VAR
594+
3 1
595+
EXP* 3
596+
597+
CON
598+
1 1
599+
L= 1
600+
601+
OBJACOORD
602+
3
603+
2 1.0
604+
1 2.0
605+
0 3.0
606+
607+
ACOORD
608+
3
609+
0 2 1.0
610+
0 1 2.0
611+
0 0 3.0
612+
613+
BCOORD
614+
1
615+
0 0.0
616+
617+
"""
618+
seekstart(io)
619+
model2 = CBF.Model()
620+
read!(io, model2)
621+
y = MOI.get(model2, MOI.ListOfVariableIndices())
622+
obj_y = MOI.get(model2, MOI.ObjectiveFunction{typeof(f)}())
623+
@test (obj_y, 1.0 * y[1] + 2.0 * y[2] + 3.0 * y[3])
624+
return
625+
end
626+
503627
function runtests()
504628
for name in names(@__MODULE__, all = true)
505629
if startswith("$(name)", "test_")

test/FileFormats/CBF/models/example_C.cbf

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,16 @@ OBJSENSE
55
MAX
66

77
VAR
8-
16 8
8+
10 6
99
F 1
1010
L= 1
1111
L+ 1
1212
L- 1
1313
Q 3
1414
QR 3
15-
EXP 3
16-
EXP* 3
1715

1816
OBJACOORD
19-
16
17+
10
2018
0 1.0
2119
1 1.0
2220
2 1.0
@@ -27,12 +25,6 @@ OBJACOORD
2725
7 1.0
2826
8 1.0
2927
9 1.0
30-
10 1.0
31-
11 1.0
32-
12 1.0
33-
13 1.0
34-
14 1.0
35-
15 1.0
3628

3729
OBJBCOORD
3830
-1.0
-146 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)