From 9e45aff0f8f9968d08eb87b4c7688e929b785c80 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 15 Apr 2024 16:01:17 +1200 Subject: [PATCH 1/8] [FileFormats.LP] write indicator constraints to file --- src/FileFormats/LP/LP.jl | 90 +++++++++++++++++++++++++++++++++++++-- test/FileFormats/LP/LP.jl | 35 +++++++++++++++ 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/FileFormats/LP/LP.jl b/src/FileFormats/LP/LP.jl index a7e7d8e981..7c64033465 100644 --- a/src/FileFormats/LP/LP.jl +++ b/src/FileFormats/LP/LP.jl @@ -27,18 +27,41 @@ function _print_shortest(io::IO, x::Float64) return end +const _ILT1{T} = MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.LessThan{T}} +const _IGT1{T} = MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.GreaterThan{T}} +const _IET1{T} = MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.EqualTo{T}} +const _ILT0{T} = MOI.Indicator{MOI.ACTIVATE_ON_ZERO,MOI.LessThan{T}} +const _IGT0{T} = MOI.Indicator{MOI.ACTIVATE_ON_ZERO,MOI.GreaterThan{T}} +const _IET0{T} = MOI.Indicator{MOI.ACTIVATE_ON_ZERO,MOI.EqualTo{T}} + MOI.Utilities.@model( Model, (MOI.ZeroOne, MOI.Integer), (MOI.EqualTo, MOI.GreaterThan, MOI.LessThan, MOI.Interval), (), - (MOI.SOS1, MOI.SOS2), + (MOI.SOS1, MOI.SOS2, _ILT1, _IET1, _IGT1, _ILT0, _IGT0, _IET0), (), (MOI.ScalarQuadraticFunction, MOI.ScalarAffineFunction), (MOI.VectorOfVariables,), - () + (MOI.VectorAffineFunction,) ) +function MOI.supports_constraint( + ::Model{T}, + ::Type{MOI.VectorAffineFunction{T}}, + ::Type{MOI.SOS1{T}}, +) where {T} + return false +end + +function MOI.supports_constraint( + ::Model{T}, + ::Type{MOI.VectorAffineFunction{T}}, + ::Type{MOI.SOS2{T}}, +) where {T} + return false +end + struct Options maximum_length::Int warn::Bool @@ -98,6 +121,7 @@ function _write_function( ::Model, func::MOI.ScalarAffineFunction{Float64}, variable_names::Dict{MOI.VariableIndex,String}; + print_one::Bool = true, kwargs..., ) is_first_item = true @@ -108,7 +132,9 @@ function _write_function( for term in func.terms if !(term.coefficient ≈ 0.0) if is_first_item - _print_shortest(io, term.coefficient) + if print_one || !isone(term.coefficient) + _print_shortest(io, term.coefficient) + end is_first_item = false else print(io, term.coefficient < 0 ? " - " : " + ") @@ -338,6 +364,63 @@ function _write_constraint( return end +function _write_indicator_constraints( + io, + model, + ::Type{S}, + variable_names, +) where {S} + F = MOI.VectorAffineFunction{Float64} + for A in (MOI.ACTIVATE_ON_ONE, MOI.ACTIVATE_ON_ZERO) + Set = MOI.Indicator{A,S} + for index in MOI.get(model, MOI.ListOfConstraintIndices{F,Set}()) + _write_constraint( + io, + model, + index, + variable_names; + write_name = true, + ) + end + end + F = MOI.VectorOfVariables + for A in (MOI.ACTIVATE_ON_ONE, MOI.ACTIVATE_ON_ZERO) + Set = MOI.Indicator{A,S} + for index in MOI.get(model, MOI.ListOfConstraintIndices{F,Set}()) + _write_constraint( + io, + model, + index, + variable_names; + write_name = true, + ) + end + end + return +end + +function _write_constraint( + io::IO, + model::Model{T}, + index::MOI.ConstraintIndex{F,MOI.Indicator{A,S}}, + variable_names::Dict{MOI.VariableIndex,String}; + write_name::Bool = true, +) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}},A,S} + func = MOI.get(model, MOI.ConstraintFunction(), index) + set = MOI.get(model, MOI.ConstraintSet(), index) + if write_name + print(io, MOI.get(model, MOI.ConstraintName(), index), ": ") + end + z, f = MOI.Utilities.scalarize(func) + flag = A == MOI.ACTIVATE_ON_ONE ? 1 : 0 + _write_function(io, model, z, variable_names; print_one = false) + # print(io, variable_names[z], + print(io, " = ", flag, " -> ") + _write_function(io, model, f, variable_names) + _write_constraint_suffix(io, set.set) + return +end + """ Base.write(io::IO, model::FileFormats.LP.Model) @@ -364,6 +447,7 @@ function Base.write(io::IO, model::Model) println(io, "subject to") for S in _SCALAR_SETS _write_constraints(io, model, S, variable_names) + _write_indicator_constraints(io, model, S, variable_names) end println(io, "Bounds") CI = MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne} diff --git a/test/FileFormats/LP/LP.jl b/test/FileFormats/LP/LP.jl index 476738dee5..147c71439c 100644 --- a/test/FileFormats/LP/LP.jl +++ b/test/FileFormats/LP/LP.jl @@ -388,6 +388,41 @@ c: 1.1 * x + 1.2 * y + -1.1 * x * x + 1.5*x*y + 1.3 in Interval(-1.1, 1.4) return end +function test_write_indicator() + model = LP.Model() + MOI.Utilities.loadfromstring!( + model, + """ + variables: x, z + c1: [z, x] in Indicator{ACTIVATE_ON_ONE}(LessThan(0.0)) + c2: [z, x] in Indicator{ACTIVATE_ON_ZERO}(GreaterThan(2.0)) + c3: [z, x] in Indicator{ACTIVATE_ON_ONE}(EqualTo(1.2)) + + c4: [z, 2.0 * x] in Indicator{ACTIVATE_ON_ONE}(LessThan(0.0)) + c5: [z, 3.0 * x] in Indicator{ACTIVATE_ON_ZERO}(GreaterThan(2.0)) + c6: [1.0 * z, x] in Indicator{ACTIVATE_ON_ONE}(EqualTo(1.2)) + z in ZeroOne() + """, + ) + MOI.write_to_file(model, LP_TEST_FILE) + @test read(LP_TEST_FILE, String) == + "minimize\n" * + "obj: \n" * + "subject to\n" * + "c4: z = 1 -> 2 x <= 0\n" * + "c1: z = 1 -> x <= 0\n" * + "c5: z = 0 -> 3 x >= 2\n" * + "c2: z = 0 -> x >= 2\n" * + "c6: z = 1 -> 1 x = 1.2\n" * + "c3: z = 1 -> x = 1.2\n" * + "Bounds\n" * + "x free\n" * + "Binary\n" * + "z\n" * + "End\n" + return +end + ### ### Read tests ### From 34b437f5a48e514ad853e6b2032cb055928fdc5a Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 15 Apr 2024 17:27:10 +1200 Subject: [PATCH 2/8] Add support for reading --- src/FileFormats/LP/LP.jl | 15 +++++++++++++++ test/FileFormats/LP/LP.jl | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/FileFormats/LP/LP.jl b/src/FileFormats/LP/LP.jl index 7c64033465..511fe43e5c 100644 --- a/src/FileFormats/LP/LP.jl +++ b/src/FileFormats/LP/LP.jl @@ -540,6 +540,7 @@ mutable struct _ReadCache num_constraints::Int name_to_variable::Dict{String,MOI.VariableIndex} has_default_bound::Set{MOI.VariableIndex} + indicator::Union{Nothing,Pair{MOI.VariableIndex,MOI.ActivationCondition}} function _ReadCache() return new( MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], 0.0), @@ -550,6 +551,7 @@ mutable struct _ReadCache 0, Dict{String,MOI.VariableIndex}(), Set{MOI.VariableIndex}(), + nothing, ) end end @@ -768,6 +770,14 @@ function _parse_section( cache.constraint_name = "R$(cache.num_constraints)" end end + if cache.indicator === nothing + if (m = match(r"\s*(.+?)\s*=\s*(0|1)\s*->(.+)", line)) !== nothing + z = _get_variable_from_name(model, cache, String(m[1])) + cond = m[2] == "0" ? MOI.ACTIVATE_ON_ZERO : MOI.ACTIVATE_ON_ONE + cache.indicator = z => cond + line = String(m[3]) + end + end if occursin("^", line) # Simplify parsing of constraints with ^2 terms by turning them into # explicit " ^ 2" terms. This avoids ambiguity when parsing names. @@ -807,6 +817,10 @@ function _parse_section( cache.constraint_function.constant, ) end + if cache.indicator !== nothing + f = MOI.Utilities.operate(vcat, Float64, cache.indicator[1], f) + constraint_set = MOI.Indicator{cache.indicator[2]}(constraint_set) + end c = MOI.add_constraint(model, f, constraint_set) MOI.set(model, MOI.ConstraintName(), c, cache.constraint_name) cache.num_constraints += 1 @@ -814,6 +828,7 @@ function _parse_section( empty!(cache.quad_terms) cache.constraint_function.constant = 0.0 cache.constraint_name = "" + cache.indicator = nothing end return end diff --git a/test/FileFormats/LP/LP.jl b/test/FileFormats/LP/LP.jl index 147c71439c..56e4b54a9d 100644 --- a/test/FileFormats/LP/LP.jl +++ b/test/FileFormats/LP/LP.jl @@ -1011,6 +1011,41 @@ function test_read_variable_bounds() return end +function test_read_indicator() + io = IOBuffer(""" + minimize + obj: 1 x + subject to + c: z = 1 -> x >= 0 + d: z = 0 -> x - y <= 1.2 + bounds + x free + z free + binary + z + end + """) + model = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_LP) + read!(io, model) + io = IOBuffer() + write(io, model) + seekstart(io) + @test read(io, String) == """ + minimize + obj: 1 x + subject to + d: z = 0 -> 1 x - 1 y <= 1.2 + c: z = 1 -> 1 x >= 0 + Bounds + x free + y >= 0 + Binary + z + End + """ + return +end + function runtests() for name in names(@__MODULE__, all = true) if startswith("$(name)", "test_") From a4012c947a12945f15ffbd27b07adeab5e588e77 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 15 Apr 2024 19:14:47 +1200 Subject: [PATCH 3/8] Update --- test/FileFormats/LP/LP.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/FileFormats/LP/LP.jl b/test/FileFormats/LP/LP.jl index 56e4b54a9d..cf66dbe017 100644 --- a/test/FileFormats/LP/LP.jl +++ b/test/FileFormats/LP/LP.jl @@ -1046,6 +1046,20 @@ function test_read_indicator() return end +function test_VectorAffineFunction_SOS() + model = MOI.FileFormats.LP.Model() + x = MOI.add_variables(3) + f = MOI.Utilities.operate(vcat, Float64, (1.0 .* x)...) + for set in (MOI.SOS1([1.0, 2.0, 3.0]), MOI.SOS2([1.0, 2.0, 3.0])) + @test !MOI.supports_constraint(model, f, set) + @test_throws( + MOI.UnsupportedConstraint{typeof(f),type(set)}, + MOI.add_constraint(model, f, set), + ) + end + return +end + function runtests() for name in names(@__MODULE__, all = true) if startswith("$(name)", "test_") From d73a292bc70114dc372b73af280bce84bf12ab57 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 15 Apr 2024 20:11:23 +1200 Subject: [PATCH 4/8] Update test/FileFormats/LP/LP.jl --- test/FileFormats/LP/LP.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FileFormats/LP/LP.jl b/test/FileFormats/LP/LP.jl index cf66dbe017..2719aa59ab 100644 --- a/test/FileFormats/LP/LP.jl +++ b/test/FileFormats/LP/LP.jl @@ -1048,7 +1048,7 @@ end function test_VectorAffineFunction_SOS() model = MOI.FileFormats.LP.Model() - x = MOI.add_variables(3) + x = MOI.add_variables(model, 3) f = MOI.Utilities.operate(vcat, Float64, (1.0 .* x)...) for set in (MOI.SOS1([1.0, 2.0, 3.0]), MOI.SOS2([1.0, 2.0, 3.0])) @test !MOI.supports_constraint(model, f, set) From 62d1997a51bf68ba405675f91ac72cbc992869a7 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 15 Apr 2024 20:46:30 +1200 Subject: [PATCH 5/8] Update test/FileFormats/LP/LP.jl --- test/FileFormats/LP/LP.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FileFormats/LP/LP.jl b/test/FileFormats/LP/LP.jl index 2719aa59ab..b6cc4582da 100644 --- a/test/FileFormats/LP/LP.jl +++ b/test/FileFormats/LP/LP.jl @@ -1051,7 +1051,7 @@ function test_VectorAffineFunction_SOS() x = MOI.add_variables(model, 3) f = MOI.Utilities.operate(vcat, Float64, (1.0 .* x)...) for set in (MOI.SOS1([1.0, 2.0, 3.0]), MOI.SOS2([1.0, 2.0, 3.0])) - @test !MOI.supports_constraint(model, f, set) + @test !MOI.supports_constraint(model, typeof(f), typeof(set)) @test_throws( MOI.UnsupportedConstraint{typeof(f),type(set)}, MOI.add_constraint(model, f, set), From f9a6e3a1902479aa7f2e347544f72fea36f41615 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 15 Apr 2024 21:32:06 +1200 Subject: [PATCH 6/8] Update test/FileFormats/LP/LP.jl --- test/FileFormats/LP/LP.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FileFormats/LP/LP.jl b/test/FileFormats/LP/LP.jl index b6cc4582da..33a1ca5946 100644 --- a/test/FileFormats/LP/LP.jl +++ b/test/FileFormats/LP/LP.jl @@ -1053,7 +1053,7 @@ function test_VectorAffineFunction_SOS() for set in (MOI.SOS1([1.0, 2.0, 3.0]), MOI.SOS2([1.0, 2.0, 3.0])) @test !MOI.supports_constraint(model, typeof(f), typeof(set)) @test_throws( - MOI.UnsupportedConstraint{typeof(f),type(set)}, + MOI.UnsupportedConstraint{typeof(f),typeof(set)}, MOI.add_constraint(model, f, set), ) end From e774c22e59f3f9e690d0dfbc9dcc76abf7478948 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 15 Apr 2024 21:48:22 +1200 Subject: [PATCH 7/8] Update src/FileFormats/LP/LP.jl --- src/FileFormats/LP/LP.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/FileFormats/LP/LP.jl b/src/FileFormats/LP/LP.jl index 511fe43e5c..5468f82fed 100644 --- a/src/FileFormats/LP/LP.jl +++ b/src/FileFormats/LP/LP.jl @@ -414,7 +414,6 @@ function _write_constraint( z, f = MOI.Utilities.scalarize(func) flag = A == MOI.ACTIVATE_ON_ONE ? 1 : 0 _write_function(io, model, z, variable_names; print_one = false) - # print(io, variable_names[z], print(io, " = ", flag, " -> ") _write_function(io, model, f, variable_names) _write_constraint_suffix(io, set.set) From 3b149470144325c7f79eb94d196651bcc3d4b9eb Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 15 Apr 2024 22:27:10 +1200 Subject: [PATCH 8/8] Update LP.jl --- test/FileFormats/LP/LP.jl | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/FileFormats/LP/LP.jl b/test/FileFormats/LP/LP.jl index 33a1ca5946..09317a9286 100644 --- a/test/FileFormats/LP/LP.jl +++ b/test/FileFormats/LP/LP.jl @@ -1048,15 +1048,9 @@ end function test_VectorAffineFunction_SOS() model = MOI.FileFormats.LP.Model() - x = MOI.add_variables(model, 3) - f = MOI.Utilities.operate(vcat, Float64, (1.0 .* x)...) - for set in (MOI.SOS1([1.0, 2.0, 3.0]), MOI.SOS2([1.0, 2.0, 3.0])) - @test !MOI.supports_constraint(model, typeof(f), typeof(set)) - @test_throws( - MOI.UnsupportedConstraint{typeof(f),typeof(set)}, - MOI.add_constraint(model, f, set), - ) - end + F = MOI.VectorAffineFunction{Float64} + @test !MOI.supports_constraint(model, F, MOI.SOS1{Float64}) + @test !MOI.supports_constraint(model, F, MOI.SOS2{Float64}) return end