From 0ba68e07fb91fe427ff80a9658ea5940cc90a922 Mon Sep 17 00:00:00 2001 From: odow Date: Thu, 5 Dec 2019 16:54:19 -0600 Subject: [PATCH] Add support for the MOP file format --- Manifest.toml | 145 +++++++++++++++++++++++++++++++++++++++ src/CBF/CBF.jl | 11 +++ src/LP/LP.jl | 12 ++++ src/MPS/MPS.jl | 159 ++++++++++++++++++++++++++++++------------- src/MathOptFormat.jl | 7 +- test/MPS/MPS.jl | 2 +- 6 files changed, 286 insertions(+), 50 deletions(-) create mode 100644 Manifest.toml diff --git a/Manifest.toml b/Manifest.toml new file mode 100644 index 0000000..76e4570 --- /dev/null +++ b/Manifest.toml @@ -0,0 +1,145 @@ +# This file is machine-generated - editing it directly is not advised + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[BenchmarkTools]] +deps = ["JSON", "Printf", "Statistics"] +git-tree-sha1 = "90b73db83791c5f83155016dd1cc1f684d4e1361" +uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +version = "0.4.3" + +[[BinaryProvider]] +deps = ["Libdl", "SHA"] +git-tree-sha1 = "5b08ed6036d9d3f0ee6369410b830f8873d4024c" +uuid = "b99e7846-7c00-51b0-8f62-c81ae34c0232" +version = "0.5.8" + +[[CodecBzip2]] +deps = ["BinaryProvider", "Libdl", "TranscodingStreams"] +git-tree-sha1 = "5db086e510c11b4c87d05067627eadb2dc079995" +uuid = "523fee87-0ab8-5b00-afb7-3ecf72e48cfd" +version = "0.6.0" + +[[CodecZlib]] +deps = ["BinaryProvider", "Libdl", "TranscodingStreams"] +git-tree-sha1 = "05916673a2627dd91b4969ff8ba6941bc85a960e" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.6.0" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[HTTP]] +deps = ["Base64", "Dates", "IniFile", "MbedTLS", "Sockets"] +git-tree-sha1 = "5c49dab19938b119fe204fd7d7e8e174f4e9c68b" +uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" +version = "0.8.8" + +[[IniFile]] +deps = ["Test"] +git-tree-sha1 = "098e4d2c533924c921f9f9847274f2ad89e018b8" +uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f" +version = "0.5.0" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.0" + +[[JSONSchema]] +deps = ["HTTP", "JSON", "Test"] +git-tree-sha1 = "6f80c4ea9ccf648766380b28b63efd1050dee940" +uuid = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" +version = "0.1.1" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[LinearAlgebra]] +deps = ["Libdl"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[MathOptInterface]] +deps = ["BenchmarkTools", "LinearAlgebra", "OrderedCollections", "SparseArrays", "Test", "Unicode"] +git-tree-sha1 = "2cbf5d6dde94a85dd28ef702ef637079dc587728" +repo-rev = "od/multi-objective" +repo-url = "https://github.com/JuliaOpt/MathOptInterface.jl.git" +uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +version = "0.9.7" + +[[MbedTLS]] +deps = ["BinaryProvider", "Dates", "Distributed", "Libdl", "Random", "Sockets", "Test"] +git-tree-sha1 = "2d94286a9c2f52c63a16146bb86fd6cdfbf677c6" +uuid = "739be429-bea8-5141-9913-cc70e7f3736d" +version = "0.6.8" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[OrderedCollections]] +deps = ["Random", "Serialization", "Test"] +git-tree-sha1 = "c4c13474d23c60d20a67b217f1d7f22a40edf8f1" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.1.0" + +[[Parsers]] +deps = ["Dates", "Test"] +git-tree-sha1 = "0139ba59ce9bc680e2925aec5b7db79065d60556" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "0.3.10" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[SparseArrays]] +deps = ["LinearAlgebra", "Random"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[Statistics]] +deps = ["LinearAlgebra", "SparseArrays"] +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[Test]] +deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[TranscodingStreams]] +deps = ["Random", "Test"] +git-tree-sha1 = "7c53c35547de1c5b9d46a4797cf6d8253807108c" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.9.5" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/src/CBF/CBF.jl b/src/CBF/CBF.jl index 0fe30d6..2cf8e82 100644 --- a/src/CBF/CBF.jl +++ b/src/CBF/CBF.jl @@ -29,6 +29,17 @@ function MOI.supports_constraint( return true end +function MOI.supports( + ::InnerModel, + ::MOI.ObjectiveFunction{<:Union{ + MOI.VectorOfVariables, + MOI.VectorAffineFunction{Float64}, + MOI.VectorQuadraticFunction{Float64} + }} +) + return false +end + struct Options end get_options(m::InnerModel) = get(m.ext, :CBF_OPTIONS, Options()) diff --git a/src/LP/LP.jl b/src/LP/LP.jl index 1bc0d14..45bfff8 100644 --- a/src/LP/LP.jl +++ b/src/LP/LP.jl @@ -16,6 +16,18 @@ MOI.Utilities.@model(InnerModel, () ) +function MOI.supports( + ::InnerModel, + ::MOI.ObjectiveFunction{<:Union{ + MOI.ScalarQuadraticFunction{Float64}, + MOI.VectorOfVariables, + MOI.VectorAffineFunction{Float64}, + MOI.VectorQuadraticFunction{Float64} + }} +) + return false +end + struct Options maximum_length::Int warn::Bool diff --git a/src/MPS/MPS.jl b/src/MPS/MPS.jl index 66e42f0..d5bc36f 100644 --- a/src/MPS/MPS.jl +++ b/src/MPS/MPS.jl @@ -16,6 +16,16 @@ MOI.Utilities.@model(InnerModel, () ) +function MOI.supports( + ::InnerModel, + ::MOI.ObjectiveFunction{<:Union{ + MOI.ScalarQuadraticFunction, + MOI.VectorQuadraticFunction + }} +) + return false +end + struct Options warn::Bool end @@ -32,7 +42,7 @@ Keyword arguments are: - `warn::Bool=false`: print a warning when variables or constraints are renamed. """ function Model(; - warn::Bool = false + warn::Bool = false ) model = InnerModel{Float64}() model.ext[:MPS_OPTIONS] = Options(warn) @@ -55,7 +65,10 @@ function Base.write(io::IO, model::InnerModel) MathOptFormat.create_unique_names( model; warn = options.warn, - replacements = Function[s -> replace(s, ' ' => '_')] + replacements = Function[ + s -> replace(s, ' ' => '_') + s -> replace(s, r"^OBJ" => "_OBJ") + ] ) write_model_name(io, model) write_rows(io, model) @@ -90,7 +103,15 @@ const LINEAR_CONSTRAINTS = ( ) function write_rows(io::IO, model::InnerModel) - println(io, "ROWS\n N OBJ") + obj_type = MOI.get(model, MOI.ObjectiveFunctionType()) + obj_func = MOI.get(model, MOI.ObjectiveFunction{obj_type}()) + if MOI.output_dimension(obj_func) == 1 + println(io, "ROWS\n N OBJ") + else + for i = 1:MOI.output_dimension(obj_func) + println(io, "ROWS\n N OBJ$(i)") + end + end for (set_type, sense_char) in LINEAR_CONSTRAINTS for index in MOI.get(model, MOI.ListOfConstraintIndices{ MOI.ScalarAffineFunction{Float64}, @@ -132,8 +153,12 @@ function add_coefficient(coefficients, variable_name, row_name, coefficient) end function extract_terms( - model::InnerModel, coefficients, row_name::String, - func::MOI.ScalarAffineFunction, discovered_columns::Set{String}) + model::InnerModel, + coefficients, + row_name::String, + func::MOI.ScalarAffineFunction, + discovered_columns::Set{String} +) for term in func.terms variable_name = MOI.get(model, MOI.VariableName(), term.variable_index) add_coefficient(coefficients, variable_name, row_name, term.coefficient) @@ -143,14 +168,35 @@ function extract_terms( end function extract_terms( - model::InnerModel, coefficients, row_name::String, func::MOI.SingleVariable, - discovered_columns::Set{String}) + model::InnerModel, + coefficients, + row_name::String, + func::MOI.SingleVariable, + discovered_columns::Set{String} +) variable_name = MOI.get(model, MOI.VariableName(), func.variable) add_coefficient(coefficients, variable_name, row_name, 1.0) push!(discovered_columns, variable_name) return end +function extract_terms( + model::InnerModel, + coefficients, + row_name::String, + func::MOI.VectorAffineFunction, + discovered_columns::Set{String} +) + for v_term in func.terms + term = v_term.scalar_term + c_name = row_name * v_term.output_index + v_name = MOI.get(model, MOI.VariableName(), term.variable_index) + add_coefficient(coefficients, v_name, c_name, term.coefficient) + push!(discovered_columns, v_name) + end + return +end + function write_columns(io::IO, model::InnerModel) # Many MPS readers (e.g., CPLEX and GAMS) will error if a variable (column) # appears in the BOUNDS section but did not appear in the COLUMNS section. @@ -163,13 +209,17 @@ function write_columns(io::IO, model::InnerModel) println(io, "COLUMNS") coefficients = Dict{String, Vector{Tuple{String, Float64}}}() for (set_type, sense_char) in LINEAR_CONSTRAINTS - for index in MOI.get(model, MOI.ListOfConstraintIndices{ - MOI.ScalarAffineFunction{Float64}, - set_type}()) + for index in MOI.get( + model, + MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, set_type + }() + ) row_name = MOI.get(model, MOI.ConstraintName(), index) func = MOI.get(model, MOI.ConstraintFunction(), index) extract_terms( - model, coefficients, row_name, func, discovered_columns) + model, coefficients, row_name, func, discovered_columns + ) end end obj_func_type = MOI.get(model, MOI.ObjectiveFunctionType()) @@ -180,7 +230,7 @@ function write_columns(io::IO, model::InnerModel) # coefficients. for (v_name, terms) in coefficients for (idx, (row_name, coef)) in enumerate(terms) - if row_name == "OBJ" + if startswith(row_name, "OBJ") terms[idx] = (row_name, -coef) end end @@ -459,12 +509,19 @@ end mutable struct TempMPSModel name::String - obj_name::String + obj_names::Vector{String} columns::Dict{String, TempColumn} rows::Dict{String, TempRow} intorg_flag::Bool # A flag used to parse COLUMNS section. - TempMPSModel() = new("", "", Dict{String, TempColumn}(), - Dict{String, TempRow}(), false) + function TempMPSModel() + return new( + "", + String[], + Dict{String, TempColumn}(), + Dict{String, TempRow}(), + false + ) + end end const HEADERS = ("ROWS", "COLUMNS", "RHS", "RANGES", "BOUNDS", "SOS", "ENDATA") @@ -546,33 +603,45 @@ function copy_to(model::InnerModel, temp::TempMPSModel) end # Add linear constraints. for (c_name, row) in temp.rows - if c_name == temp.obj_name - # Set objective. - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - obj_func = if length(row.terms) == 1 && - first(row.terms).second == 1.0 - MOI.SingleVariable(variable_map[first(row.terms).first]) - else - MOI.ScalarAffineFunction([ - MOI.ScalarAffineTerm(coef, variable_map[v_name]) - for (v_name, coef) in row.terms], - 0.0) - end - MOI.set(model, MOI.ObjectiveFunction{typeof(obj_func)}(), obj_func) + if c_name in temp.obj_names + continue + end + constraint_function = MOI.ScalarAffineFunction([ + MOI.ScalarAffineTerm(coef, variable_map[v_name]) + for (v_name, coef) in row.terms], + 0.0) + set = bounds_to_set(row.lower, row.upper) + if set !== nothing + c = MOI.add_constraint(model, constraint_function, set) + MOI.set(model, MOI.ConstraintName(), c, c_name) else - constraint_function = MOI.ScalarAffineFunction([ - MOI.ScalarAffineTerm(coef, variable_map[v_name]) - for (v_name, coef) in row.terms], - 0.0) - set = bounds_to_set(row.lower, row.upper) - if set !== nothing - c = MOI.add_constraint(model, constraint_function, set) - MOI.set(model, MOI.ConstraintName(), c, c_name) - else - error("Expected a non-empty set for $(c_name). Got row=$(row)") + error("Expected a non-empty set for $(c_name). Got row=$(row)") + end + end + # Set objective. + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + obj_func = if length(temp.obj_names) == 1 + row = temp.rows[first(temp.obj_names)] + MOI.ScalarAffineFunction( + [MOI.ScalarAffineTerm(coef, variable_map[v_name]) for (v_name, coef) in row.terms], + 0.0 + ) + else + v_terms = MOI.VectorAffineTerm{Float64}[] + for (i, name) in enumerate(temp.obj_names) + row = temp.rows[name] + for (v_name, coef) in row.terms + push!( + v_terms, + MOI.VectorAffineTerm( + i, MOI.ScalarAffineTerm(coef, variable_map[v_name]) + ) + ) end end + MOI.VectorAffineFunction(v_terms, zeros(length(temp.obj_names))) end + MOI.set(model, MOI.ObjectiveFunction{typeof(obj_func)}(), obj_func) return end @@ -603,20 +672,14 @@ function parse_rows_line(data::TempMPSModel, items::Vector{String}) sense, name = items if haskey(data.rows, name) error("Duplicate row encountered: $(line).") - elseif sense != "N" && sense != "L" && sense != "G" && sense != "E" + elseif !(sense in ("N", "L", "G", "E")) error("Invalid row sense: $(join(items, " "))") end row = TempRow() row.sense = sense if sense == "N" - if data.obj_name != "" - # Detected a duplicate objective. Skip it. - return name - end - data.obj_name = name - end - # Add some default bounds for the constraints. - if sense == "G" + push!(data.obj_names, name) + elseif sense == "G" row.lower = 0.0 elseif sense == "L" row.upper = 0.0 @@ -635,7 +698,7 @@ end function parse_single_coefficient(data, row_name, column_name, value) row = get(data.rows, row_name, nothing) if row === nothing - error("ROW name $(row_name) not recognised. Is it in the ROWS field?") + error("Row name $(row_name) not recognised. Is it in the ROWS field?") end if haskey(row.terms, column_name) row.terms[column_name] += parse(Float64, value) diff --git a/src/MathOptFormat.jl b/src/MathOptFormat.jl index 1b308c1..fdde338 100644 --- a/src/MathOptFormat.jl +++ b/src/MathOptFormat.jl @@ -23,6 +23,7 @@ List of accepted export formats. - `FORMAT_CBF`: the Conic Benchmark format - `FORMAT_LP`: the LP file format - `FORMAT_MOF`: the MathOptFormat file format +- `FORMAT_MOP`: the Multi-Objective Problem file format - `FORMAT_MPS`: the MPS file format - `FORMAT_SDPA`: the SemiDefinite Programming Algorithm format """ @@ -32,6 +33,7 @@ List of accepted export formats. FORMAT_CBF, FORMAT_LP, FORMAT_MOF, + FORMAT_MOP, FORMAT_MPS, FORMAT_SDPA, ) @@ -63,6 +65,8 @@ function Model( return LP.Model(; kwargs...) elseif format == FORMAT_MOF return MOF.Model(; kwargs...) + elseif format == FORMAT_MOP + return MPS.Model(; kwargs...) elseif format == FORMAT_MPS return MPS.Model(; kwargs...) elseif format == FORMAT_SDPA @@ -76,8 +80,9 @@ function Model( (".cbf", CBF.Model), (".lp", LP.Model), (".mof.json", MOF.Model), + (".mop", MPS.Model), (".mps", MPS.Model), - (".sdpa", SDPA.Model) + (".sdpa", SDPA.Model), ] if endswith(filename, ext) || occursin("$(ext).", filename) return model(; kwargs...) diff --git a/test/MPS/MPS.jl b/test/MPS/MPS.jl index c2481dc..6a4ba28 100644 --- a/test/MPS/MPS.jl +++ b/test/MPS/MPS.jl @@ -97,7 +97,7 @@ end model_2 = MPS.Model() MOIU.loadfromstring!(model_2, """ variables: x, y, z - minobjective: x + y + z + minobjective: [x + y + z, x + 2 * y] con1: 1.0 * x in Interval(1.0, 5.0) con2: 1.0 * x in Interval(2.0, 6.0) con3: 1.0 * x in Interval(3.0, 7.0)