From 3c234dbc9831f0dd8ffc0e54de288c117556c5e2 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 14 Sep 2026 11:29:43 +1200 Subject: [PATCH 1/3] [FileFormats.MPS] add support for semi- variables --- src/FileFormats/MPS/MPS.jl | 11 +++- src/FileFormats/MPS/read.jl | 25 ++++++++- src/FileFormats/MPS/write.jl | 41 +++++++++++--- test/FileFormats/MPS/test_MPS.jl | 93 +++++++++++++++++++++++++++++--- 4 files changed, 153 insertions(+), 17 deletions(-) diff --git a/src/FileFormats/MPS/MPS.jl b/src/FileFormats/MPS/MPS.jl index 41939d19f4..4f7133cb47 100644 --- a/src/FileFormats/MPS/MPS.jl +++ b/src/FileFormats/MPS/MPS.jl @@ -60,7 +60,7 @@ end function MOI.supports_constraint( ::Model, ::Type{MOI.VariableIndex}, - ::Type{<:Union{MOI.Parameter,MOI.Semicontinuous,MOI.Semiinteger}}, + ::Type{<:MOI.Parameter}, ) return false end @@ -170,7 +170,14 @@ end Base.summary(io::IO, ::Model) = print(io, "MOI.FileFormats.MPS.Model") -@enum(VType, VTYPE_CONTINUOUS, VTYPE_INTEGER, VTYPE_BINARY) +@enum( + VType, + VTYPE_CONTINUOUS, + VTYPE_INTEGER, + VTYPE_BINARY, + VTYPE_SEMICONTINUOUS, + VTYPE_SEMIINTEGER, +) include("read.jl") include("write.jl") diff --git a/src/FileFormats/MPS/read.jl b/src/FileFormats/MPS/read.jl index f623c457d5..f1612b3424 100644 --- a/src/FileFormats/MPS/read.jl +++ b/src/FileFormats/MPS/read.jl @@ -448,6 +448,15 @@ function _add_variable(model::Model{T}, data, variable_map, i, name) where {T} x = MOI.add_variable(model) variable_map[name] = x MOI.set(model, MOI.VariableName(), x, name) + if data.vtype[i] == VTYPE_SEMIINTEGER + set = MOI.Semiinteger(data.col_lower[i], data.col_upper[i]) + MOI.add_constraint(model, x, set) + return + elseif data.vtype[i] == VTYPE_SEMICONTINUOUS + set = MOI.Semicontinuous(data.col_lower[i], data.col_upper[i]) + MOI.add_constraint(model, x, set) + return + end set = bounds_to_set(data.col_lower[i], data.col_upper[i]) if set isa MOI.Interval # Do not add MOI.Interval constraints because we want to follow JuMP's @@ -926,7 +935,11 @@ function _parse_single_bound( data.col_lower[col] = value elseif bound_type == "LI" data.col_lower[col] = value - data.vtype[col] = VTYPE_INTEGER + if data.vtype[col] in (VTYPE_SEMICONTINUOUS, VTYPE_SEMIINTEGER) + data.vtype[col] = VTYPE_SEMIINTEGER + else + data.vtype[col] = VTYPE_INTEGER + end elseif bound_type == "UI" data.col_upper[col] = value data.vtype[col] = VTYPE_INTEGER @@ -943,6 +956,16 @@ function _parse_single_bound( data.col_lower[col] = typemin(T) data.col_upper[col] = typemax(T) data.vtype[col] = VTYPE_BINARY + elseif bound_type == "SC" + data.col_upper[col] = value + if data.vtype[col] == VTYPE_INTEGER + data.vtype[col] = VTYPE_SEMIINTEGER + else + data.vtype[col] = VTYPE_SEMICONTINUOUS + end + elseif bound_type == "SI" + data.col_upper[col] = value + data.vtype[col] = VTYPE_SEMIINTEGER else _throw_parse_error( data, diff --git a/src/FileFormats/MPS/write.jl b/src/FileFormats/MPS/write.jl index b071bb581c..fc37bac531 100644 --- a/src/FileFormats/MPS/write.jl +++ b/src/FileFormats/MPS/write.jl @@ -546,11 +546,8 @@ end # UP upper bound x <= b # UI integer variable x <= b # BV binary variable x = 0 or 1 -# -# Not yet implemented: -# -# SC semi-cont variable x = 0 or l <= x <= b -# l is the lower bound on the variable. If none set then defaults to 1 +# SC semicontinuous variable x in {0} ∪ [LO, b] +# SI semiinteger variable x in {0} ∪ [LI, b] # ============================================================================== function write_single_bound( @@ -560,7 +557,29 @@ function write_single_bound( upper::T, vtype, ) where {T} - if lower == upper + if vtype == VTYPE_SEMICONTINUOUS + write_single_bound(io, var_name, lower, typemax(T), VTYPE_CONTINUOUS) + println( + io, + Card( + f1 = "SC", + f2 = "bounds", + f3 = var_name, + f4 = _to_string(upper), + ), + ) + elseif vtype == VTYPE_SEMIINTEGER + write_single_bound(io, var_name, lower, typemax(T), VTYPE_INTEGER) + println( + io, + Card( + f1 = "SI", + f2 = "bounds", + f3 = var_name, + f4 = _to_string(upper), + ), + ) + elseif lower == upper println( io, Card( @@ -613,6 +632,14 @@ update_bounds(x, set::MOI.EqualTo) = (set.value, set.value, x[3]) update_bounds(x, set::MOI.ZeroOne) = (x[1], x[2], VTYPE_BINARY) +function update_bounds(x, set::MOI.Semicontinuous) + return (set.lower, set.upper, VTYPE_SEMICONTINUOUS) +end + +function update_bounds(x, set::MOI.Semiinteger) + return (set.lower, set.upper, VTYPE_SEMIINTEGER) +end + function _collect_bounds(bounds, model, ::Type{S}, var_to_column) where {S} for index in MOI.get(model, MOI.ListOfConstraintIndices{MOI.VariableIndex,S}()) @@ -636,6 +663,8 @@ function write_bounds(io::IO, model::Model{T}, var_to_column) where {T} _collect_bounds(bounds, model, MOI.EqualTo{T}, var_to_column) _collect_bounds(bounds, model, MOI.Interval{T}, var_to_column) _collect_bounds(bounds, model, MOI.ZeroOne, var_to_column) + _collect_bounds(bounds, model, MOI.Semiinteger{T}, var_to_column) + _collect_bounds(bounds, model, MOI.Semicontinuous{T}, var_to_column) for (variable, column) in var_to_column var_name = _var_name(model, variable, column, options.generic_names) lower, upper, vtype = bounds[column] diff --git a/test/FileFormats/MPS/test_MPS.jl b/test/FileFormats/MPS/test_MPS.jl index 7b067b7ca1..d0d9d37a52 100644 --- a/test/FileFormats/MPS/test_MPS.jl +++ b/test/FileFormats/MPS/test_MPS.jl @@ -1539,14 +1539,6 @@ function test_unsupported_variable_types() MOI.UnsupportedConstraint, MOI.add_constrained_variable(model, MOI.Parameter(2.0)), ) - @test_throws( - MOI.UnsupportedConstraint, - MOI.add_constrained_variable(model, MOI.Semicontinuous(2.0, 3.0)), - ) - @test_throws( - MOI.UnsupportedConstraint, - MOI.add_constrained_variable(model, MOI.Semiinteger(2.0, 3.0)), - ) return end @@ -1840,6 +1832,91 @@ function test_unsupported_kwarg() return end +function test_round_trip_semiinteger() + _test_model_equality( + """ + variables: x + minobjective: 1.0 * x + x in Semicontinuous(1.0, 2.0) + """, + ["x"], + String[], + ) + return +end + +function test_semi_reading_1() + LB, UB = [-1.0, 0.0, 1.0], [-0.5, 0.0, 0.5, 1.5] + for lb in LB, ub in UB, SC in ["SC", "SI"], LO in ["LO", "LI"] + if lb > ub + continue + end + l_row = iszero(lb) ? "" : "$LO bounds x $lb\n" + io = IOBuffer( + """ + NAME + ROWS + N obj + COLUMNS + x obj 1.0 + RHS + BOUNDS + $(l_row)$(SC) bounds x $ub + ENDATA + """, + ) + target = if SC == "SC" && (iszero(lb) || LO == "LO") + MOI.Semicontinuous(lb, ub) + else + MOI.Semiinteger(lb, ub) + end + model = MPS.Model() + read!(io, model) + dest = MOI.Utilities.Model{Float64}() + MOI.copy_to(dest, model) + x = MOI.get(dest, MOI.VariableIndex, "x") + ci = MOI.ConstraintIndex{MOI.VariableIndex,typeof(target)}(x.value) + @test MOI.get(dest, MOI.ConstraintSet(), ci) == target + end + return +end + +function test_semi_reading_2() + LB, UB = [-1.0, 0.0, 1.0], [-0.5, 0.0, 0.5, 1.5] + for lb in LB, ub in UB, SC in ["SC", "SI"], LO in ["LO", "LI"] + if lb > ub + continue + end + l_row = iszero(lb) ? "" : "$LO bounds x $lb\n" + io = IOBuffer( + """ + NAME + ROWS + N obj + COLUMNS + x obj 1.0 + RHS + BOUNDS + $(SC) bounds x $ub + $(l_row)ENDATA + """, + ) + target = if SC == "SC" && (iszero(lb) || LO == "LO") + MOI.Semicontinuous(lb, ub) + else + MOI.Semiinteger(lb, ub) + end + model = MPS.Model() + read!(io, model) + dest = MOI.Utilities.Model{Float64}() + MOI.copy_to(dest, model) + x = MOI.get(dest, MOI.VariableIndex, "x") + ci = MOI.ConstraintIndex{MOI.VariableIndex,typeof(target)}(x.value) + @test MOI.get(dest, MOI.ConstraintSet(), ci) == target + end + return +end + end # TestMPS TestMPS.runtests() From 7af649275fd34457ca9b6db47a27585e60a3ec0e Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 14 Sep 2026 11:46:19 +1200 Subject: [PATCH 2/3] Fix formatting --- test/FileFormats/MPS/test_MPS.jl | 52 +++++++++++++++----------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/test/FileFormats/MPS/test_MPS.jl b/test/FileFormats/MPS/test_MPS.jl index d0d9d37a52..51913a5059 100644 --- a/test/FileFormats/MPS/test_MPS.jl +++ b/test/FileFormats/MPS/test_MPS.jl @@ -1852,26 +1852,24 @@ function test_semi_reading_1() continue end l_row = iszero(lb) ? "" : "$LO bounds x $lb\n" - io = IOBuffer( - """ - NAME - ROWS - N obj - COLUMNS - x obj 1.0 - RHS - BOUNDS - $(l_row)$(SC) bounds x $ub - ENDATA - """, - ) + src = """ + NAME + ROWS + N obj + COLUMNS + x obj 1.0 + RHS + BOUNDS + $(l_row)$(SC) bounds x $ub + ENDATA + """ target = if SC == "SC" && (iszero(lb) || LO == "LO") MOI.Semicontinuous(lb, ub) else MOI.Semiinteger(lb, ub) end model = MPS.Model() - read!(io, model) + read!(IOBuffer(src), model) dest = MOI.Utilities.Model{Float64}() MOI.copy_to(dest, model) x = MOI.get(dest, MOI.VariableIndex, "x") @@ -1888,26 +1886,24 @@ function test_semi_reading_2() continue end l_row = iszero(lb) ? "" : "$LO bounds x $lb\n" - io = IOBuffer( - """ - NAME - ROWS - N obj - COLUMNS - x obj 1.0 - RHS - BOUNDS - $(SC) bounds x $ub - $(l_row)ENDATA - """, - ) + src = """ + NAME + ROWS + N obj + COLUMNS + x obj 1.0 + RHS + BOUNDS + $(SC) bounds x $ub + $(l_row)ENDATA + """ target = if SC == "SC" && (iszero(lb) || LO == "LO") MOI.Semicontinuous(lb, ub) else MOI.Semiinteger(lb, ub) end model = MPS.Model() - read!(io, model) + read!(IOBuffer(src), model) dest = MOI.Utilities.Model{Float64}() MOI.copy_to(dest, model) x = MOI.get(dest, MOI.VariableIndex, "x") From 79518bd6f346e309fbaea9c9541888dbd7c98fd5 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 14 Sep 2026 13:51:16 +1200 Subject: [PATCH 3/3] Update --- test/FileFormats/MPS/test_MPS.jl | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/FileFormats/MPS/test_MPS.jl b/test/FileFormats/MPS/test_MPS.jl index 51913a5059..44e88b318f 100644 --- a/test/FileFormats/MPS/test_MPS.jl +++ b/test/FileFormats/MPS/test_MPS.jl @@ -1832,12 +1832,25 @@ function test_unsupported_kwarg() return end +function test_round_trip_semicontinuous() + _test_model_equality( + """ + variables: x + minobjective: 1.0 * x + x in Semicontinuous(2.0, 3.0) + """, + ["x"], + String[], + ) + return +end + function test_round_trip_semiinteger() _test_model_equality( """ variables: x minobjective: 1.0 * x - x in Semicontinuous(1.0, 2.0) + x in Semiinteger(2.0, 3.0) """, ["x"], String[],