Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 91 additions & 38 deletions src/FileFormats/LP/LP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,46 @@ function _write_bounds(io, model, S, variable_names, free_variables)
return
end

function _write_sos_constraints(io, model, variable_names)
T, F = Float64, MOI.VectorOfVariables
sos1_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,MOI.SOS1{T}}())
sos2_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,MOI.SOS2{T}}())
if length(sos1_indices) + length(sos2_indices) == 0
return
end
println(io, "SOS")
for index in sos1_indices
_write_constraint(io, model, index, variable_names)
end
for index in sos2_indices
_write_constraint(io, model, index, variable_names)
end
return
end

_to_string(::Type{MOI.SOS1{Float64}}) = "S1::"
_to_string(::Type{MOI.SOS2{Float64}}) = "S2::"

function _write_constraint(
io::IO,
model::Model,
index::MOI.ConstraintIndex{MOI.VectorOfVariables,S},
variable_names::Dict{MOI.VariableIndex,String},
) where {S<:Union{MOI.SOS1{Float64},MOI.SOS2{Float64}}}
f = MOI.get(model, MOI.ConstraintFunction(), index)
s = MOI.get(model, MOI.ConstraintSet(), index)
name = MOI.get(model, MOI.ConstraintName(), index)
if name !== nothing && !isempty(name)
print(io, name, ": ")
end
print(io, _to_string(S))
for (w, x) in zip(s.weights, f.variables)
print(io, " ", variable_names[x], ":", w)
end
println(io)
return
end

"""
Base.write(io::IO, model::FileFormats.LP.Model)

Expand Down Expand Up @@ -280,6 +320,7 @@ function Base.write(io::IO, model::Model)
end
_write_integrality(io, model, "General", MOI.Integer, variable_names)
_write_integrality(io, model, "Binary", MOI.ZeroOne, variable_names)
_write_sos_constraints(io, model, variable_names)
println(io, "End")
return
end
Expand All @@ -295,6 +336,7 @@ const _KW_CONSTRAINTS = Val{:constraints}()
const _KW_BOUNDS = Val{:bounds}()
const _KW_INTEGER = Val{:integer}()
const _KW_BINARY = Val{:binary}()
const _KW_SOS = Val{:sos}()
const _KW_END = Val{:end}()

const _KEYWORDS = Dict(
Expand Down Expand Up @@ -323,6 +365,8 @@ const _KEYWORDS = Dict(
"bin" => _KW_BINARY,
"binary" => _KW_BINARY,
"binaries" => _KW_BINARY,
# _KW_SOS
"sos" => _KW_SOS,
# _KW_END
"end" => _KW_END,
)
Expand Down Expand Up @@ -473,50 +517,16 @@ end

# _KW_CONSTRAINTS

function _parse_sos_constraint(
model::Model,
cache::_ReadCache,
line::AbstractString,
)
tokens = _tokenize(line)
if length(tokens) < 3
error("Malformed SOS constraint: $(line)")
end
name = String(split(tokens[1], ":")[1])
if tokens[2] == "S1::"
order = 1
elseif tokens[2] == "S2::"
order = 2
else
error("SOS of type $(tokens[2]) not recognised")
end
variables, weights = MOI.VariableIndex[], Float64[]
for token in tokens[3:end]
items = String.(split(token, ":"))
if length(items) != 2
error("Invalid sequence: $(token)")
end
push!(variables, _get_variable_from_name(model, cache, items[1]))
push!(weights, parse(Float64, items[2]))
end
c_ref = if tokens[2] == "S1::"
MOI.add_constraint(model, variables, MOI.SOS1(weights))
else
@assert tokens[2] == "S2::"
MOI.add_constraint(model, variables, MOI.SOS2(weights))
end
MOI.set(model, MOI.ConstraintName(), c_ref, name)
return
end

function _parse_section(
::typeof(_KW_CONSTRAINTS),
model::Model,
cache::_ReadCache,
line::AbstractString,
)
if match(r" S([1-2]):: ", line) !== nothing
_parse_sos_constraint(model, cache, line)
# SOS constraints should be in their own "SOS" section, but we can also
# recognize them if they're mixed into the constraint section.
if match(r" S([1-2])\w*:: ", line) !== nothing
_parse_section(_KW_SOS, model, cache, line)
return
end
if isempty(cache.constraint_name)
Expand Down Expand Up @@ -650,6 +660,49 @@ function _parse_section(::typeof(_KW_BINARY), model, cache, line)
return
end

# _KW_SOS

function _parse_section(
::typeof(_KW_SOS),
model::Model,
cache::_ReadCache,
line::AbstractString,
)
# SOS constraints can have all manner of whitespace issues with them.
# Normalize them here before attempting to do anything else.
line = replace(line, r"\s+:\s+" => ":")
line = replace(line, r"\s+::" => "::")
tokens = _tokenize(line)
if length(tokens) < 3
error("Malformed SOS constraint: $(line)")
end
name = String(split(tokens[1], ":")[1])
if tokens[2] == "S1::"
order = 1
elseif tokens[2] == "S2::"
order = 2
else
error("SOS of type $(tokens[2]) not recognised")
end
variables, weights = MOI.VariableIndex[], Float64[]
for token in tokens[3:end]
items = String.(split(token, ":"))
if length(items) != 2
error("Invalid sequence: $(token)")
end
push!(variables, _get_variable_from_name(model, cache, items[1]))
push!(weights, parse(Float64, items[2]))
end
c_ref = if tokens[2] == "S1::"
MOI.add_constraint(model, variables, MOI.SOS1(weights))
else
@assert tokens[2] == "S2::"
MOI.add_constraint(model, variables, MOI.SOS2(weights))
end
MOI.set(model, MOI.ConstraintName(), c_ref, name)
return
end

# _KW_END

function _parse_section(
Expand Down
7 changes: 7 additions & 0 deletions test/FileFormats/LP/LP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ c7: 1.5a + 1.6 == 0.2
c8: 1.7a + 1.8 in Interval(0.3, 0.4)
x in ZeroOne()
y in Integer()
c11: [x, y, z] in SOS1{Float64}([1.0, 2.0, 3.0])
c12: [x, y, z] in SOS2{Float64}([3.3, 1.1, 2.2])
""",
)
MOI.write_to_file(model, LP_TEST_FILE)
Expand All @@ -50,6 +52,9 @@ y in Integer()
"y\n" *
"Binary\n" *
"x\n" *
"SOS\n" *
"c11: S1:: x:1.0 y:2.0 z:3.0\n" *
"c12: S2:: x:3.3 y:1.1 z:2.2\n" *
"End\n"

@test !MOI.is_empty(model)
Expand Down Expand Up @@ -308,6 +313,8 @@ function test_read_model1_tricky()
@test occursin("\nV5\n", file)
@test occursin("\nV6\n", file)
@test occursin("Binary\nV8\n", file)
@test occursin("sos1: S1:: V1:1.0 V2:2.0 V3:3.0", file)
@test occursin("sos2: S2:: V1:8.5 V2:10.2 V3:18.3", file)
return
end

Expand Down
5 changes: 3 additions & 2 deletions test/FileFormats/LP/models/invalid_sos_constraint.lp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ CON1: 1 V1 >= 0.0
CON2: 1 V2 >= 2.0
CON3: 1 V3 <= 2.5
CON4: 1 V5 + 1 V6 + 1 V7 <= 1.0
csos1: S1:: V1:1 V3:2 V5:3
csos2: S2::
SOS
csos1: S1:: V1:1 V3:2 V5:3
csos2: S2::
Bounds
-inf <= V1 <= 3
-inf <= V2 <= 3
Expand Down
5 changes: 3 additions & 2 deletions test/FileFormats/LP/models/invalid_sos_set.lp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ CON1: 1 V1 >= 0.0
CON2: 1 V2 >= 2.0
CON3: 1 V3 <= 2.5
CON4: 1 V5 + 1 V6 + 1 V7 <= 1.0
csos1: S1:: V1:1 V3:2 V5:3
csos2: S3:: V2:2 V4:1 V5:2.5
SOS
csos1: S1:: V1:1 V3:2 V5:3
csos2: S3:: V2:2 V4:1 V5:2.5
Bounds
-inf <= V1 <= 3
-inf <= V2 <= 3
Expand Down
3 changes: 3 additions & 0 deletions test/FileFormats/LP/models/model1_tricky.lp
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@ Var4 V5 \ integer variables can be listed (MOSEK)
V6 \ or each new line
Binary
V8
SOS
sos1: S1:: V1 : 1 V2 : 2 V3 : 3
sos2: S2 :: V1:8.5 V2:10.2 V3:18.3
End