Skip to content

Commit 3ca230f

Browse files
authored
Add write_to_file and read_from_file (#2114)
* Add write_to_file and read_from_file * Update Project.toml and add file_formats.jl to runtests.jl * Add read/write to io streams * Update docstrings * Change to Base.read and Base.write * Updates for MathOptFormat v0.4 * Fixes for MathOptFormat 0.4 * Update docs and fix copy_to
1 parent d2f6a29 commit 3ca230f

File tree

7 files changed

+213
-2
lines changed

7 files changed

+213
-2
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9"
88
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
99
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
1010
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
11+
MathOptFormat = "f4570300-c277-12e8-125c-4912f86ce65d"
1112
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
1213
NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
1314
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
@@ -18,6 +19,7 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1819
Calculus = "0.5"
1920
DataStructures = "0.17"
2021
ForwardDiff = "~0.5.0, ~0.6, ~0.7, ~0.8, ~0.9, ~0.10"
22+
MathOptFormat = "0.4"
2123
MathOptInterface = "~0.9.1"
2224
NaNMath = "0.3"
2325
OffsetArrays = "≥ 0.2.13"

docs/src/solvers.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,19 @@ set_time_limit_sec
112112
unset_time_limit_sec
113113
time_limit_sec
114114
```
115+
116+
## File formats
117+
118+
JuMP can write models to a variety of file-formats using [`write_to_file`](@ref)
119+
and [`Base.write`](@ref).
120+
```@docs
121+
write_to_file
122+
Base.write(::IO, ::Model; ::JuMP.MathOptFormat.FileFormat)
123+
```
124+
125+
JuMP models can be created from file formats using [`read_from_file`](@ref) and
126+
[`Base.read`](@ref).
127+
```@docs
128+
read_from_file
129+
Base.read(::IO, ::Type{Model}; ::JuMP.MathOptFormat.FileFormat)
130+
```

src/JuMP.jl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const MOIU = MOI.Utilities
2020
import Calculus
2121
import DataStructures.OrderedDict
2222
import ForwardDiff
23+
import MathOptFormat
2324
include("_Derivatives/_Derivatives.jl")
2425
using ._Derivatives
2526

@@ -38,8 +39,12 @@ Base.@deprecate(setlowerbound, JuMP.set_lower_bound)
3839
Base.@deprecate(setupperbound, JuMP.set_upper_bound)
3940
Base.@deprecate(linearterms, JuMP.linear_terms)
4041

41-
writeLP(args...; kargs...) = error("writeLP has been removed from JuMP. Use `MathOptFormat.jl` instead.")
42-
writeMPS(args...; kargs...) = error("writeMPS has been removed from JuMP. Use `MathOptFormat.jl` instead.")
42+
function writeLP(args...; kargs...)
43+
error("writeLP has been removed from JuMP. Use `write_to_file` instead.")
44+
end
45+
function writeMPS(args...; kargs...)
46+
error("writeMPS has been removed from JuMP. Use `write_to_file` instead.")
47+
end
4348

4449
include("utils.jl")
4550

@@ -787,6 +792,7 @@ include("nlp.jl")
787792
include("print.jl")
788793
include("lp_sensitivity.jl")
789794
include("callbacks.jl")
795+
include("file_formats.jl")
790796

791797
# JuMP exports everything except internal symbols, which are defined as those
792798
# whose name starts with an underscore. If you don't want all of these symbols

src/copy.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,11 @@ end
144144
function Base.deepcopy(::Model)
145145
error("`JuMP.Model` does not support `deepcopy` as the reference to the underlying solver cannot be deep copied, use `copy` instead.")
146146
end
147+
148+
function MOI.copy_to(dest::MOI.ModelLike, src::Model)
149+
return MOI.copy_to(dest, backend(src))
150+
end
151+
152+
function MOI.copy_to(dest::Model, src::MOI.ModelLike)
153+
return MOI.copy_to(backend(dest), src)
154+
end

src/file_formats.jl

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
"""
7+
write_to_file(
8+
model::Model,
9+
filename::String;
10+
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_AUTOMATIC
11+
)
12+
13+
Write the JuMP model `model` to `filename` in the format `format`.
14+
15+
If the filename ends in `.gz`, it will be compressed using Gzip.
16+
If the filename ends in `.bz2`, it will be compressed using BZip2.
17+
"""
18+
function write_to_file(
19+
model::Model,
20+
filename::String;
21+
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_AUTOMATIC
22+
)
23+
dest = MathOptFormat.Model(format = format, filename = filename)
24+
# We add a `full_bridge_optimizer` here because MathOptFormat models may not
25+
# support all constraint types in a JuMP model.
26+
bridged_dest = MOI.Bridges.full_bridge_optimizer(dest, Float64)
27+
MOI.copy_to(bridged_dest, model)
28+
# `dest` will contain the underlying model, with constraints bridged if
29+
# necessary.
30+
MOI.write_to_file(dest, filename)
31+
return
32+
end
33+
34+
"""
35+
Base.write(
36+
io::IO,
37+
model::Model;
38+
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_MOF
39+
)
40+
41+
Write the JuMP model `model` to `io` in the format `format`.
42+
"""
43+
function Base.write(
44+
io::IO,
45+
model::Model;
46+
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_MOF
47+
)
48+
if format == MathOptFormat.FORMAT_AUTOMATIC
49+
error("Unable to infer the file format from an IO stream.")
50+
end
51+
dest = MathOptFormat.Model(format = format)
52+
# We add a `full_bridge_optimizer` here because MathOptFormat models may not
53+
# support all constraint types in a JuMP model.
54+
bridged_dest = MOI.Bridges.full_bridge_optimizer(dest, Float64)
55+
MOI.copy_to(bridged_dest, model)
56+
# `dest` will contain the underlying model, with constraints bridged if
57+
# necessary.
58+
write(io, dest)
59+
return
60+
end
61+
62+
"""
63+
read_from_file(
64+
filename::String;
65+
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_AUTOMATIC
66+
)
67+
68+
Return a JuMP model read from `filename` in the format `format`.
69+
70+
If the filename ends in `.gz`, it will be uncompressed using Gzip.
71+
If the filename ends in `.bz2`, it will be uncompressed using BZip2.
72+
"""
73+
function read_from_file(
74+
filename::String;
75+
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_AUTOMATIC
76+
)
77+
src = MathOptFormat.Model(format = format, filename = filename)
78+
MOI.read_from_file(src, filename)
79+
model = Model()
80+
MOI.copy_to(model, src)
81+
return model
82+
end
83+
84+
"""
85+
Base.read(io::IO, ::Type{Model}; format::MathOptFormat.FileFormat)
86+
87+
Return a JuMP model read from `io` in the format `format`.
88+
"""
89+
function Base.read(io::IO, ::Type{Model}; format::MathOptFormat.FileFormat)
90+
if format == MathOptFormat.FORMAT_AUTOMATIC
91+
error("Unable to infer the file format from an IO stream.")
92+
end
93+
src = MathOptFormat.Model(format = format)
94+
read!(io, src)
95+
model = Model()
96+
MOI.copy_to(model, src)
97+
return model
98+
end

test/file_formats.jl

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
using JuMP
7+
using MathOptFormat
8+
using Test
9+
10+
@testset "File formats" begin
11+
@testset "MOF" begin
12+
model = Model()
13+
@variable(model, x)
14+
@constraint(model, my_c, 3 * x >= 1)
15+
@objective(model, Min, 2 * x^2 + x + 1)
16+
write_to_file(model, "my_model.mof.json")
17+
model_2 = read_from_file("my_model.mof.json")
18+
@test sprint(print, model) == sprint(print, model_2)
19+
rm("my_model.mof.json")
20+
end
21+
@testset "MPS" begin
22+
model = Model()
23+
@variable(model, x >= 0)
24+
@constraint(model, my_c, 3 * x >= 1)
25+
@objective(model, Min, 2 * x)
26+
write_to_file(model, "my_model.mps")
27+
model_2 = read_from_file("my_model.mps")
28+
@test sprint(print, model) == sprint(print, model_2)
29+
rm("my_model.mps")
30+
end
31+
@testset "LP" begin
32+
model = Model()
33+
@variable(model, x >= 0)
34+
@constraint(model, my_c, 3 * x >= 1)
35+
@objective(model, Min, 2 * x)
36+
write_to_file(model, "my_model.lp")
37+
@test read("my_model.lp", String) ==
38+
"minimize\nobj: 2 x\nsubject to\nmy_c: 3 x >= 1\nBounds\nx >= 0\nEnd\n"
39+
@test_throws(
40+
ErrorException("read! is not implemented for LP files."),
41+
read_from_file("my_model.lp")
42+
)
43+
rm("my_model.lp")
44+
end
45+
@testset "CBF" begin
46+
model = Model()
47+
@variable(model, X[1:2, 1:2], PSD)
48+
@constraint(model, my_c, sum(X) >= 1)
49+
@objective(model, Min, sum(X))
50+
write_to_file(model, "my_model.cbf")
51+
@test read("my_model.cbf", String) ==
52+
"VER\n3\n\nOBJSENSE\nMIN\n\nVAR\n3 1\nF 3\n\nOBJACOORD\n3\n0 1.0\n1 2.0\n2 1.0\n\nCON\n1 1\nL+ 1\n\nACOORD\n3\n0 0 1.0\n0 1 2.0\n0 2 1.0\n\nBCOORD\n1\n0 -1.0\n\nPSDCON\n1\n2\n\nHCOORD\n3\n0 0 0 0 1.0\n0 1 1 0 1.0\n0 2 1 1 1.0\n\n"
53+
model_2 = read_from_file("my_model.cbf")
54+
# Note: we replace ' in ' => ' ∈ ' because the unicode doesn't print on
55+
# Windows systems for some reason.
56+
@test replace(sprint(print, model_2), " in " => "") ==
57+
"Min noname + 2 noname + noname\nSubject to\n [noname + 2 noname + noname - 1] ∈ MathOptInterface.Nonnegatives(1)\n [noname, noname, noname] ∈ MathOptInterface.PositiveSemidefiniteConeTriangle(2)\n"
58+
rm("my_model.cbf")
59+
end
60+
@testset "Base read/write via io" begin
61+
model = Model()
62+
@variable(model, x)
63+
@constraint(model, my_c, 3 * x >= 1)
64+
@objective(model, Min, 2 * x^2 + x + 1)
65+
io = IOBuffer()
66+
@test_throws(
67+
ErrorException("Unable to infer the file format from an IO stream."),
68+
write(io, model; format = MathOptFormat.FORMAT_AUTOMATIC)
69+
)
70+
write(io, model; format = MathOptFormat.FORMAT_MOF)
71+
seekstart(io)
72+
@test_throws(
73+
ErrorException("Unable to infer the file format from an IO stream."),
74+
read(io, Model; format = MathOptFormat.FORMAT_AUTOMATIC)
75+
)
76+
seekstart(io)
77+
model_2 = read(io, Model; format = MathOptFormat.FORMAT_MOF)
78+
@test sprint(print, model) == sprint(print, model_2)
79+
end
80+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ include("operator.jl")
4040
include("macros.jl")
4141
include("lp_sensitivity.jl")
4242
include("callbacks.jl")
43+
include("file_formats.jl")
4344
# TODO: The hygiene test should run in a separate Julia instance where JuMP hasn't been loaded via `using`.
4445
include("hygiene.jl")

0 commit comments

Comments
 (0)