Skip to content

Commit

Permalink
Add write_to_file and read_from_file (#2114)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
odow authored Dec 9, 2019
1 parent d2f6a29 commit 3ca230f
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MathOptFormat = "f4570300-c277-12e8-125c-4912f86ce65d"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Expand All @@ -18,6 +19,7 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Calculus = "0.5"
DataStructures = "0.17"
ForwardDiff = "~0.5.0, ~0.6, ~0.7, ~0.8, ~0.9, ~0.10"
MathOptFormat = "0.4"
MathOptInterface = "~0.9.1"
NaNMath = "0.3"
OffsetArrays = "≥ 0.2.13"
Expand Down
16 changes: 16 additions & 0 deletions docs/src/solvers.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,19 @@ set_time_limit_sec
unset_time_limit_sec
time_limit_sec
```

## File formats

JuMP can write models to a variety of file-formats using [`write_to_file`](@ref)
and [`Base.write`](@ref).
```@docs
write_to_file
Base.write(::IO, ::Model; ::JuMP.MathOptFormat.FileFormat)
```

JuMP models can be created from file formats using [`read_from_file`](@ref) and
[`Base.read`](@ref).
```@docs
read_from_file
Base.read(::IO, ::Type{Model}; ::JuMP.MathOptFormat.FileFormat)
```
10 changes: 8 additions & 2 deletions src/JuMP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const MOIU = MOI.Utilities
import Calculus
import DataStructures.OrderedDict
import ForwardDiff
import MathOptFormat
include("_Derivatives/_Derivatives.jl")
using ._Derivatives

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

writeLP(args...; kargs...) = error("writeLP has been removed from JuMP. Use `MathOptFormat.jl` instead.")
writeMPS(args...; kargs...) = error("writeMPS has been removed from JuMP. Use `MathOptFormat.jl` instead.")
function writeLP(args...; kargs...)
error("writeLP has been removed from JuMP. Use `write_to_file` instead.")
end
function writeMPS(args...; kargs...)
error("writeMPS has been removed from JuMP. Use `write_to_file` instead.")
end

include("utils.jl")

Expand Down Expand Up @@ -787,6 +792,7 @@ include("nlp.jl")
include("print.jl")
include("lp_sensitivity.jl")
include("callbacks.jl")
include("file_formats.jl")

# JuMP exports everything except internal symbols, which are defined as those
# whose name starts with an underscore. If you don't want all of these symbols
Expand Down
8 changes: 8 additions & 0 deletions src/copy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,11 @@ end
function Base.deepcopy(::Model)
error("`JuMP.Model` does not support `deepcopy` as the reference to the underlying solver cannot be deep copied, use `copy` instead.")
end

function MOI.copy_to(dest::MOI.ModelLike, src::Model)
return MOI.copy_to(dest, backend(src))
end

function MOI.copy_to(dest::Model, src::MOI.ModelLike)
return MOI.copy_to(backend(dest), src)
end
98 changes: 98 additions & 0 deletions src/file_formats.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

"""
write_to_file(
model::Model,
filename::String;
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_AUTOMATIC
)
Write the JuMP model `model` to `filename` in the format `format`.
If the filename ends in `.gz`, it will be compressed using Gzip.
If the filename ends in `.bz2`, it will be compressed using BZip2.
"""
function write_to_file(
model::Model,
filename::String;
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_AUTOMATIC
)
dest = MathOptFormat.Model(format = format, filename = filename)
# We add a `full_bridge_optimizer` here because MathOptFormat models may not
# support all constraint types in a JuMP model.
bridged_dest = MOI.Bridges.full_bridge_optimizer(dest, Float64)
MOI.copy_to(bridged_dest, model)
# `dest` will contain the underlying model, with constraints bridged if
# necessary.
MOI.write_to_file(dest, filename)
return
end

"""
Base.write(
io::IO,
model::Model;
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_MOF
)
Write the JuMP model `model` to `io` in the format `format`.
"""
function Base.write(
io::IO,
model::Model;
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_MOF
)
if format == MathOptFormat.FORMAT_AUTOMATIC
error("Unable to infer the file format from an IO stream.")
end
dest = MathOptFormat.Model(format = format)
# We add a `full_bridge_optimizer` here because MathOptFormat models may not
# support all constraint types in a JuMP model.
bridged_dest = MOI.Bridges.full_bridge_optimizer(dest, Float64)
MOI.copy_to(bridged_dest, model)
# `dest` will contain the underlying model, with constraints bridged if
# necessary.
write(io, dest)
return
end

"""
read_from_file(
filename::String;
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_AUTOMATIC
)
Return a JuMP model read from `filename` in the format `format`.
If the filename ends in `.gz`, it will be uncompressed using Gzip.
If the filename ends in `.bz2`, it will be uncompressed using BZip2.
"""
function read_from_file(
filename::String;
format::MathOptFormat.FileFormat = MathOptFormat.FORMAT_AUTOMATIC
)
src = MathOptFormat.Model(format = format, filename = filename)
MOI.read_from_file(src, filename)
model = Model()
MOI.copy_to(model, src)
return model
end

"""
Base.read(io::IO, ::Type{Model}; format::MathOptFormat.FileFormat)
Return a JuMP model read from `io` in the format `format`.
"""
function Base.read(io::IO, ::Type{Model}; format::MathOptFormat.FileFormat)
if format == MathOptFormat.FORMAT_AUTOMATIC
error("Unable to infer the file format from an IO stream.")
end
src = MathOptFormat.Model(format = format)
read!(io, src)
model = Model()
MOI.copy_to(model, src)
return model
end
80 changes: 80 additions & 0 deletions test/file_formats.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

using JuMP
using MathOptFormat
using Test

@testset "File formats" begin
@testset "MOF" begin
model = Model()
@variable(model, x)
@constraint(model, my_c, 3 * x >= 1)
@objective(model, Min, 2 * x^2 + x + 1)
write_to_file(model, "my_model.mof.json")
model_2 = read_from_file("my_model.mof.json")
@test sprint(print, model) == sprint(print, model_2)
rm("my_model.mof.json")
end
@testset "MPS" begin
model = Model()
@variable(model, x >= 0)
@constraint(model, my_c, 3 * x >= 1)
@objective(model, Min, 2 * x)
write_to_file(model, "my_model.mps")
model_2 = read_from_file("my_model.mps")
@test sprint(print, model) == sprint(print, model_2)
rm("my_model.mps")
end
@testset "LP" begin
model = Model()
@variable(model, x >= 0)
@constraint(model, my_c, 3 * x >= 1)
@objective(model, Min, 2 * x)
write_to_file(model, "my_model.lp")
@test read("my_model.lp", String) ==
"minimize\nobj: 2 x\nsubject to\nmy_c: 3 x >= 1\nBounds\nx >= 0\nEnd\n"
@test_throws(
ErrorException("read! is not implemented for LP files."),
read_from_file("my_model.lp")
)
rm("my_model.lp")
end
@testset "CBF" begin
model = Model()
@variable(model, X[1:2, 1:2], PSD)
@constraint(model, my_c, sum(X) >= 1)
@objective(model, Min, sum(X))
write_to_file(model, "my_model.cbf")
@test read("my_model.cbf", String) ==
"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"
model_2 = read_from_file("my_model.cbf")
# Note: we replace ' in ' => ' ∈ ' because the unicode doesn't print on
# Windows systems for some reason.
@test replace(sprint(print, model_2), " in " => "") ==
"Min noname + 2 noname + noname\nSubject to\n [noname + 2 noname + noname - 1] ∈ MathOptInterface.Nonnegatives(1)\n [noname, noname, noname] ∈ MathOptInterface.PositiveSemidefiniteConeTriangle(2)\n"
rm("my_model.cbf")
end
@testset "Base read/write via io" begin
model = Model()
@variable(model, x)
@constraint(model, my_c, 3 * x >= 1)
@objective(model, Min, 2 * x^2 + x + 1)
io = IOBuffer()
@test_throws(
ErrorException("Unable to infer the file format from an IO stream."),
write(io, model; format = MathOptFormat.FORMAT_AUTOMATIC)
)
write(io, model; format = MathOptFormat.FORMAT_MOF)
seekstart(io)
@test_throws(
ErrorException("Unable to infer the file format from an IO stream."),
read(io, Model; format = MathOptFormat.FORMAT_AUTOMATIC)
)
seekstart(io)
model_2 = read(io, Model; format = MathOptFormat.FORMAT_MOF)
@test sprint(print, model) == sprint(print, model_2)
end
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ include("operator.jl")
include("macros.jl")
include("lp_sensitivity.jl")
include("callbacks.jl")
include("file_formats.jl")
# TODO: The hygiene test should run in a separate Julia instance where JuMP hasn't been loaded via `using`.
include("hygiene.jl")

0 comments on commit 3ca230f

Please sign in to comment.