Skip to content

Commit 18e3d1b

Browse files
authored
[FileFormats.LP] add support for quadratic problems (#1974)
1 parent 8c8636f commit 18e3d1b

File tree

4 files changed

+319
-30
lines changed

4 files changed

+319
-30
lines changed

src/FileFormats/LP/LP.jl

Lines changed: 165 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,10 @@ MOI.Utilities.@model(
3535
(),
3636
(MOI.SOS1, MOI.SOS2),
3737
(),
38-
(MOI.ScalarAffineFunction,),
38+
(MOI.ScalarQuadraticFunction, MOI.ScalarAffineFunction),
3939
(MOI.VectorOfVariables,),
4040
()
4141
)
42-
function MOI.supports(
43-
::Model{T},
44-
::MOI.ObjectiveFunction{<:MOI.ScalarQuadraticFunction{T}},
45-
) where {T}
46-
return false
47-
end
4842

4943
struct Options
5044
maximum_length::Int
@@ -93,7 +87,8 @@ function _write_function(
9387
io::IO,
9488
::Model,
9589
func::MOI.VariableIndex,
96-
variable_names::Dict{MOI.VariableIndex,String},
90+
variable_names::Dict{MOI.VariableIndex,String};
91+
kwargs...,
9792
)
9893
print(io, variable_names[func])
9994
return
@@ -103,7 +98,8 @@ function _write_function(
10398
io::IO,
10499
::Model,
105100
func::MOI.ScalarAffineFunction{Float64},
106-
variable_names::Dict{MOI.VariableIndex,String},
101+
variable_names::Dict{MOI.VariableIndex,String};
102+
kwargs...,
107103
)
108104
is_first_item = true
109105
if !(func.constant 0.0)
@@ -125,6 +121,66 @@ function _write_function(
125121
return
126122
end
127123

124+
function _write_function(
125+
io::IO,
126+
::Model,
127+
func::MOI.ScalarQuadraticFunction{Float64},
128+
variable_names::Dict{MOI.VariableIndex,String};
129+
print_half::Bool = true,
130+
kwargs...,
131+
)
132+
is_first_item = true
133+
if !(func.constant 0.0)
134+
_print_shortest(io, func.constant)
135+
is_first_item = false
136+
end
137+
for term in func.affine_terms
138+
if !(term.coefficient 0.0)
139+
if is_first_item
140+
_print_shortest(io, term.coefficient)
141+
is_first_item = false
142+
else
143+
print(io, term.coefficient < 0 ? " - " : " + ")
144+
_print_shortest(io, abs(term.coefficient))
145+
end
146+
print(io, " ", variable_names[term.variable])
147+
end
148+
end
149+
if length(func.quadratic_terms) > 0
150+
if is_first_item
151+
print(io, "[ ")
152+
else
153+
print(io, " + [ ")
154+
end
155+
is_first_item = true
156+
for term in func.quadratic_terms
157+
coefficient = term.coefficient
158+
if !print_half && term.variable_1 == term.variable_2
159+
coefficient /= 2
160+
end
161+
if is_first_item
162+
_print_shortest(io, coefficient)
163+
is_first_item = false
164+
else
165+
print(io, coefficient < 0 ? " - " : " + ")
166+
_print_shortest(io, abs(coefficient))
167+
end
168+
print(io, " ", variable_names[term.variable_1])
169+
if term.variable_1 == term.variable_2
170+
print(io, " ^ 2")
171+
else
172+
print(io, " * ", variable_names[term.variable_2])
173+
end
174+
end
175+
if print_half
176+
print(io, " ]/2")
177+
else
178+
print(io, " ]")
179+
end
180+
end
181+
return
182+
end
183+
128184
function _write_constraint_suffix(io::IO, set::MOI.LessThan)
129185
print(io, " <= ")
130186
_print_shortest(io, set.upper)
@@ -174,7 +230,7 @@ function _write_constraint(
174230
print(io, MOI.get(model, MOI.ConstraintName(), index), ": ")
175231
end
176232
_write_constraint_prefix(io, set)
177-
_write_function(io, model, func, variable_names)
233+
_write_function(io, model, func, variable_names; print_half = false)
178234
_write_constraint_suffix(io, set)
179235
return
180236
end
@@ -233,6 +289,10 @@ function _write_constraints(io, model, S, variable_names)
233289
for index in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
234290
_write_constraint(io, model, index, variable_names; write_name = true)
235291
end
292+
F = MOI.ScalarQuadraticFunction{Float64}
293+
for index in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
294+
_write_constraint(io, model, index, variable_names; write_name = true)
295+
end
236296
return
237297
end
238298

@@ -382,15 +442,19 @@ const _KEYWORDS = Dict(
382442

383443
mutable struct _ReadCache
384444
objective::MOI.ScalarAffineFunction{Float64}
445+
quad_obj_terms::Vector{MOI.ScalarQuadraticTerm{Float64}}
385446
constraint_function::MOI.ScalarAffineFunction{Float64}
447+
quad_terms::Vector{MOI.ScalarQuadraticTerm{Float64}}
386448
constraint_name::String
387449
num_constraints::Int
388450
name_to_variable::Dict{String,MOI.VariableIndex}
389451
has_default_bound::Set{MOI.VariableIndex}
390452
function _ReadCache()
391453
return new(
392454
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], 0.0),
455+
MOI.ScalarQuadraticTerm{Float64}[],
393456
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], 0.0),
457+
MOI.ScalarQuadraticTerm{Float64}[],
394458
"",
395459
0,
396460
Dict{String,MOI.VariableIndex}(),
@@ -424,13 +488,30 @@ end
424488

425489
_tokenize(line::AbstractString) = String.(split(line, " "; keepempty = false))
426490

427-
@enum(_TokenType, _TOKEN_VARIABLE, _TOKEN_COEFFICIENT, _TOKEN_SIGN)
491+
@enum(
492+
_TokenType,
493+
_TOKEN_VARIABLE,
494+
_TOKEN_COEFFICIENT,
495+
_TOKEN_SIGN,
496+
_TOKEN_QUADRATIC_OPEN,
497+
_TOKEN_QUADRATIC_CLOSE,
498+
_TOKEN_QUADRATIC_DIAG,
499+
_TOKEN_QUADRATIC_OFF_DIAG,
500+
)
428501

429502
function _parse_token(token::String)
430503
if token == "+"
431504
return _TOKEN_SIGN, +1.0
432505
elseif token == "-"
433506
return _TOKEN_SIGN, -1.0
507+
elseif startswith(token, "[")
508+
return _TOKEN_QUADRATIC_OPEN, +1.0
509+
elseif startswith(token, "]")
510+
return _TOKEN_QUADRATIC_CLOSE, 0.5
511+
elseif token == "^"
512+
return _TOKEN_QUADRATIC_DIAG, +1.0
513+
elseif token == "*"
514+
return _TOKEN_QUADRATIC_OFF_DIAG, +1.0
434515
end
435516
coef = tryparse(Float64, token)
436517
if coef === nothing
@@ -455,12 +536,30 @@ function _get_term(token_types, token_values, offset)
455536
if offset > length(token_types) || token_types[offset] == _TOKEN_SIGN
456537
return coef, offset # It's a standalone constant!
457538
end
539+
if token_types[offset] == _TOKEN_QUADRATIC_OPEN
540+
return _get_term(token_types, token_values, offset + 1)
541+
end
458542
@assert token_types[offset] == _TOKEN_VARIABLE
459543
x = MOI.VariableIndex(Int64(token_values[offset]))
460-
return MOI.ScalarAffineTerm(coef, x), offset + 1
544+
offset += 1
545+
if offset > length(token_types) || token_types[offset] == _TOKEN_SIGN
546+
return MOI.ScalarAffineTerm(coef, x), offset
547+
end
548+
term = if token_types[offset] == _TOKEN_QUADRATIC_DIAG
549+
MOI.ScalarQuadraticTerm(coef, x, x)
550+
else
551+
@assert token_types[offset] == _TOKEN_QUADRATIC_OFF_DIAG
552+
y = MOI.VariableIndex(Int64(token_values[offset+1]))
553+
MOI.ScalarQuadraticTerm(coef, x, y)
554+
end
555+
if get(token_types, offset + 2, nothing) == _TOKEN_QUADRATIC_CLOSE
556+
return term, offset + 3
557+
else
558+
return term, offset + 2
559+
end
461560
end
462561

463-
function _parse_affine_terms(
562+
function _parse_function(
464563
f::MOI.ScalarAffineFunction{Float64},
465564
model::Model,
466565
cache::_ReadCache,
@@ -474,6 +573,10 @@ function _parse_affine_terms(
474573
token_types[i] = token_type
475574
if token_type in (_TOKEN_SIGN, _TOKEN_COEFFICIENT)
476575
token_values[i] = token::Float64
576+
elseif token_type in (_TOKEN_QUADRATIC_OPEN, _TOKEN_QUADRATIC_CLOSE)
577+
token_values[i] = NaN
578+
elseif token_type in (_TOKEN_QUADRATIC_DIAG, _TOKEN_QUADRATIC_OFF_DIAG)
579+
token_values[i] = NaN
477580
else
478581
@assert token_type == _TOKEN_VARIABLE
479582
x = _get_variable_from_name(model, cache, token::String)
@@ -486,6 +589,15 @@ function _parse_affine_terms(
486589
term, offset = _get_term(token_types, token_values, offset)
487590
if term isa MOI.ScalarAffineTerm{Float64}
488591
push!(f.terms, term::MOI.ScalarAffineTerm{Float64})
592+
elseif term isa MOI.ScalarQuadraticTerm{Float64}
593+
push!(cache.quad_terms, term::MOI.ScalarQuadraticTerm{Float64})
594+
if tokens[offset-1] == "]"
595+
for (i, term) in enumerate(cache.quad_terms)
596+
x, y = term.variable_1, term.variable_2
597+
scale = (x == y ? 2 : 1) * term.coefficient
598+
cache.quad_terms[i] = MOI.ScalarQuadraticTerm(scale, x, y)
599+
end
600+
end
489601
else
490602
f.constant += term::Float64
491603
end
@@ -520,13 +632,21 @@ function _parse_section(
520632
if occursin(":", line) # Strip name of the objective
521633
line = String(match(r"(.*?)\:(.*)", line)[2])
522634
end
635+
if occursin("^", line)
636+
line = replace(line, "^" => " ^ ")
637+
end
638+
if occursin(r"\][\s/][\s/]+2", line)
639+
line = replace(line, r"\][\s/][\s/]+2" => "]/2")
640+
end
523641
tokens = _tokenize(line)
524642
if length(tokens) == 0
525643
# Can happen if the name of the objective is on one line and the
526644
# expression is on the next.
527645
return
528646
end
529-
_parse_affine_terms(cache.objective, model, cache, tokens)
647+
_parse_function(cache.objective, model, cache, tokens)
648+
append!(cache.quad_obj_terms, cache.quad_terms)
649+
empty!(cache.quad_terms)
530650
return
531651
end
532652

@@ -554,6 +674,15 @@ function _parse_section(
554674
cache.constraint_name = "R$(cache.num_constraints)"
555675
end
556676
end
677+
if occursin("^", line)
678+
# Simplify parsing of constraints with ^2 terms by turning them into
679+
# explicit " ^ 2" terms. This avoids ambiguity when parsing names.
680+
line = replace(line, "^" => " ^ ")
681+
end
682+
if occursin(r"\][\s/][\s/]+2", line)
683+
# Simplify parsing of ]/2 end blocks, which may contain whitespace.
684+
line = replace(line, r"\][\s/][\s/]+2" => "]/2")
685+
end
557686
tokens = _tokenize(line)
558687
if length(tokens) == 0
559688
# Can happen if the name is on one line and the constraint on the next.
@@ -573,12 +702,22 @@ function _parse_section(
573702
MOI.EqualTo(rhs)
574703
end
575704
end
576-
_parse_affine_terms(cache.constraint_function, model, cache, tokens)
705+
_parse_function(cache.constraint_function, model, cache, tokens)
577706
if constraint_set !== nothing
578-
c = MOI.add_constraint(model, cache.constraint_function, constraint_set)
707+
f = if isempty(cache.quad_terms)
708+
cache.constraint_function
709+
else
710+
MOI.ScalarQuadraticFunction(
711+
cache.quad_terms,
712+
cache.constraint_function.terms,
713+
cache.constraint_function.constant,
714+
)
715+
end
716+
c = MOI.add_constraint(model, f, constraint_set)
579717
MOI.set(model, MOI.ConstraintName(), c, cache.constraint_name)
580718
cache.num_constraints += 1
581719
empty!(cache.constraint_function.terms)
720+
empty!(cache.quad_terms)
582721
cache.constraint_function.constant = 0.0
583722
cache.constraint_name = ""
584723
end
@@ -795,11 +934,16 @@ function Base.read!(io::IO, model::Model)
795934
end
796935
_parse_section(section, model, cache, line)
797936
end
798-
MOI.set(
799-
model,
800-
MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
801-
cache.objective,
802-
)
937+
obj = if isempty(cache.quad_obj_terms)
938+
cache.objective
939+
else
940+
MOI.ScalarQuadraticFunction(
941+
cache.quad_obj_terms,
942+
cache.objective.terms,
943+
cache.objective.constant,
944+
)
945+
end
946+
MOI.set(model, MOI.ObjectiveFunction{typeof(obj)}(), obj)
803947
return
804948
end
805949

0 commit comments

Comments
 (0)