From 02b06349e88202f3d812b45b44012a3ab20da5b4 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 19 Apr 2021 13:22:35 +1200 Subject: [PATCH 01/11] Implement print for MOI.ModelLike --- Project.toml | 1 + src/MathOptInterface.jl | 1 + src/print.jl | 311 +++++++++++++++++++++++++++++++++++++ test/print.jl | 334 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 647 insertions(+) create mode 100644 src/print.jl create mode 100644 test/print.jl diff --git a/Project.toml b/Project.toml index edc6c1f008..540978ab90 100644 --- a/Project.toml +++ b/Project.toml @@ -11,6 +11,7 @@ JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MutableArithmetics = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/src/MathOptInterface.jl b/src/MathOptInterface.jl index 03015f7558..251da0d3c1 100644 --- a/src/MathOptInterface.jl +++ b/src/MathOptInterface.jl @@ -139,6 +139,7 @@ include("constraints.jl") include("modifications.jl") include("variables.jl") include("nlp.jl") +include("print.jl") if VERSION > v"1.4.2" include("precompile.jl") diff --git a/src/print.jl b/src/print.jl new file mode 100644 index 0000000000..41de755b8d --- /dev/null +++ b/src/print.jl @@ -0,0 +1,311 @@ +using Printf + +_drop_moi(s) = replace(string(s), "MathOptInterface." => "") + +#------------------------------------------------------------------------ +# Math Symbols +#------------------------------------------------------------------------ + +# REPL-specific symbols +# Anything here: https://en.wikipedia.org/wiki/Windows-1252 +# should probably work fine on Windows +function _to_string(::MIME, name::Symbol) + if name == :leq + return "<=" + elseif name == :geq + return ">=" + elseif name == :eq + return "==" + elseif name == :times + return "*" + elseif name == :sq + return "²" + elseif name == :in + return Sys.iswindows() ? "in" : "∈" + end +end + +function _to_string(::MIME"text/latex", name::Symbol) + if name == :leq + return "\\leq" + elseif name == :geq + return "\\geq" + elseif name == :eq + return "=" + elseif name == :times + return "\\times " + elseif name == :sq + return "^2" + elseif name == :in + return "\\in" + end +end + +#------------------------------------------------------------------------ +# Functions +#------------------------------------------------------------------------ + +function _to_string(::MIME, model::ModelLike, v::VariableIndex) + var_name = get(model, VariableName(), v) + return isempty(var_name) ? "noname" : var_name +end + +function _to_string(::MIME"text/latex", model::ModelLike, v::VariableIndex) + var_name = get(model, VariableName(), v) + if isempty(var_name) + return "noname" + end + # We need to escape latex math characters that appear in the name. + # However, it's probably impractical to catch everything, so let's just + # escape the common ones: + # Escape underscores to prevent them being treated as subscript markers. + var_name = replace(var_name, "_" => "\\_") + # Escape carets to prevent them being treated as superscript markers. + var_name = replace(var_name, "^" => "\\^") + # Convert any x[args] to x_{args} so that indices on x print as subscripts. + m = match(r"^(.*)\[(.+)\]$", var_name) + if m !== nothing + var_name = m[1] * "_{" * m[2] * "}" + end + return var_name +end + +function _to_string(mime, model::ModelLike, f::SingleVariable) + return _to_string(mime, model, f.variable) +end + +function _to_string(mime, model::ModelLike, term::ScalarAffineTerm) + name = _to_string(mime, model, term.variable_index) + if term.coefficient < 0 + return string(" - ", string(-term.coefficient), " ", name) + else + return string(" + ", string(term.coefficient), " ", name) + end +end + +function _to_string(mime, model::ModelLike, f::ScalarAffineFunction) + s = string(f.constant) + for term in f.terms + s *= _to_string(mime, model, term) + end + return s +end + +function _to_string(mime, model::ModelLike, term::ScalarQuadraticTerm) + name_1 = _to_string(mime, model, term.variable_index_1) + name_2 = _to_string(mime, model, term.variable_index_2) + name = if term.variable_index_1 == term.variable_index_2 + string(" ", name_1, _to_string(mime, :sq)) + else + string(" ", name_1, _to_string(mime, :times), name_2) + end + if term.coefficient < 0 + return string(" - ", string(-term.coefficient), name) + else + return string(" + ", string(term.coefficient), name) + end +end + +function _to_string(mime, model::ModelLike, f::ScalarQuadraticFunction) + s = string(f.constant) + for term in f.affine_terms + s *= _to_string(mime, model, term) + end + for term in f.quadratic_terms + s *= _to_string(mime, model, term) + end + return s +end + +function _to_string(mime, model::ModelLike, f::AbstractVectorFunction) + rows = map(fi -> _to_string(mime, model, fi), Utilities.eachscalar(f)) + max_length = maximum(length.(rows)) + s = join(map(r -> string("│", rpad(r, max_length), "│"), rows), '\n') + return string( + "┌", + rpad("", max_length), + "┐\n", + s, + "\n└", + rpad("", max_length), + "┘", + ) +end + +function _to_string( + mime::MIME"text/latex", + model::ModelLike, + f::AbstractVectorFunction, +) + return string( + "\\begin{bmatrix}\n", + join( + map(fi -> _to_string(mime, model, fi), Utilities.eachscalar(f)), + "\\\\\n", + ), + "\\end{bmatrix}", + ) +end + +#------------------------------------------------------------------------ +# Sets +#------------------------------------------------------------------------ + +function _to_string(mime, set::LessThan) + return string(_to_string(mime, :leq), " ", set.upper) +end + +_to_string(::MIME"text/latex", set::LessThan) = "\\le $(set.upper)" + +function _to_string(mime, set::GreaterThan) + return string(_to_string(mime, :geq), " ", set.lower) +end + +_to_string(::MIME"text/latex", set::GreaterThan) = "\\ge $(set.lower)" + +function _to_string(mime, set::EqualTo) + return string(_to_string(mime, :eq), " ", set.value) +end + +_to_string(::MIME"text/latex", set::EqualTo) = "= $(set.value)" + +function _to_string(mime, set::Interval) + return string(_to_string(mime, :in), " [", set.lower, ", ", set.upper, "]") +end + +function _to_string(::MIME"text/latex", set::Interval) + return "\\in \\[$(set.lower), $(set.upper)\\]" +end + +_to_string(::MIME, ::ZeroOne) = "∈ {0, 1}" + +_to_string(::MIME"text/latex", ::ZeroOne) = "\\in \\{0, 1\\}" + +_to_string(::MIME, ::Integer) = "∈ ℤ" + +_to_string(::MIME"text/latex", ::Integer) = "\\in \\mathbb{Z}" + +function _to_string(mime, set::AbstractSet) + return string(_to_string(mime, :in), " ", _drop_moi(set)) +end + +function _to_string(::MIME"text/latex", set::AbstractSet) + set_str = replace(replace(_drop_moi(set), "{" => "\\{"), "}" => "\\}") + return string("\\in \\text{", set_str, "}") +end + +#------------------------------------------------------------------------ +# Constraints +#------------------------------------------------------------------------ + +function _to_string(mime::MIME, model::ModelLike, cref::ConstraintIndex) + f = get(model, ConstraintFunction(), cref) + s = get(model, ConstraintSet(), cref) + return string(_to_string(mime, model, f), " ", _to_string(mime, s)) +end + +#------------------------------------------------------------------------ +# ModelLike +#------------------------------------------------------------------------ + +""" + _print_model(io::IO, model::ModelLike, mime::MIME"text/plain") + +Print a plain-text formulation of `model` to `io`. +""" +function _print_model(io::IO, model::ModelLike, mime::MIME"text/plain") + sense = get(model, ObjectiveSense()) + if sense == MAX_SENSE + F = get(model, ObjectiveFunctionType()) + println(io, "Maximize $(_drop_moi(F)):") + f = get(model, ObjectiveFunction{F}()) + println(io, " ", _to_string(mime, model, f)) + elseif sense == MIN_SENSE + F = get(model, ObjectiveFunctionType()) + println(io, "Minimize $(_drop_moi(F)):") + f = get(model, ObjectiveFunction{F}()) + println(io, " ", _to_string(mime, model, f)) + else + println(io, "Feasibility") + end + println(io, "\nSubject to:") + for (F, S) in get(model, ListOfConstraints()) + println(io, "\n$(_drop_moi(F))-in-$(_drop_moi(S))") + for cref in get(model, ListOfConstraintIndices{F,S}()) + s = _to_string(mime, model, cref) + println(io, " ", replace(s, '\n' => "\n ")) + end + end + return +end + +""" + _print_model(io::IO, model::ModelLike, mime::MIME"text/latex") + +Print a LaTeX formulation of `model` to `io`. +""" +function _print_model(io::IO, model::ModelLike, mime::MIME"text/latex") + println(io, "\$\$ \\begin{aligned}") + sense = get(model, ObjectiveSense()) + if sense == MAX_SENSE + print(io, "\\max\\quad & ") + F = get(model, ObjectiveFunctionType()) + f = get(model, ObjectiveFunction{F}()) + println(io, _to_string(mime, model, f), "\\\\") + elseif sense == MIN_SENSE + print(io, "\\min\\quad & ") + F = get(model, ObjectiveFunctionType()) + f = get(model, ObjectiveFunction{F}()) + println(io, _to_string(mime, model, f), "\\\\") + else + println(io, "\\text{feasibility}\\\\") + end + print(io, "\\text{Subject to} \\quad") + for (F, S) in get(model, ListOfConstraints()) + println(io, "$F-in-$S") + for cref in get(model, ListOfConstraintIndices{F,S}()) + println(io, " & ", _to_string(mime, model, cref), "\\\\") + end + end + return print(io, "\\end{aligned} \$\$") +end + +#------------------------------------------------------------------------ +# Latex +#------------------------------------------------------------------------ + +struct _LatexModel{T<:ModelLike} + model::T +end + +""" + latex_formulation(model::ModelLike) + +Wrap `model` in a type so that it can be pretty-printed as `text/latex` in a +notebook like IJulia, or in Documenter. + +To render the model, end the cell with `latex_formulation(model)`, or call +`display(latex_formulation(model))` in to force the display of the model from +inside a function. +""" +latex_formulation(model::ModelLike) = _LatexModel(model) + +function Base.show(io::IO, model::_LatexModel) + return _print_model(io, model.model, MIME("text/latex")) +end + +Base.show(io::IO, ::MIME"text/latex", model::_LatexModel) = show(io, model) + +function Base.print(model::ModelLike) + for d in Base.Multimedia.displays + if Base.Multimedia.displayable(d, "text/latex") && + startswith("$(typeof(d))", "IJulia.") + return display(d, "text/latex", latex_formulation(model)) + end + end + return _print_model(stdout, model, MIME("text/plain")) +end + +function Base.print(io::IO, model::ModelLike) + return _print_model(io, model, MIME("text/plain")) +end diff --git a/test/print.jl b/test/print.jl new file mode 100644 index 0000000000..89f7a7802d --- /dev/null +++ b/test/print.jl @@ -0,0 +1,334 @@ +module TestPrint + +using MathOptInterface +const MOI = MathOptInterface + +using Test + +const LATEX = MIME("text/latex") +const PLAIN = MIME("text/plain") + +function test_nonname_variable() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + @test MOI._to_string(PLAIN, model, x) == "noname" + @test MOI._to_string(LATEX, model, x) == "noname" +end + +function test_variable() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + for (name, latex_name) in + [("x", "x"), ("x_y", "x\\_y"), ("x^2", "x\\^2"), ("x[a,b]", "x_{a,b}")] + MOI.set(model, MOI.VariableName(), x, name) + @test MOI._to_string(PLAIN, model, x) == name + @test MOI._to_string(LATEX, model, x) == latex_name + end +end + +function test_single_variable() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + MOI.set(model, MOI.VariableName(), x, "x") + f = MOI.SingleVariable(x) + @test MOI._to_string(PLAIN, model, f) == "x" + @test MOI._to_string(LATEX, model, f) == "x" +end + +function test_ScalarAffineTerm() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + MOI.set(model, MOI.VariableName(), x, "x") + @test MOI._to_string(PLAIN, model, MOI.ScalarAffineTerm(-1.2, x)) == + " - 1.2 x" + @test MOI._to_string(LATEX, model, MOI.ScalarAffineTerm(1.2, x)) == + " + 1.2 x" +end + +function test_ScalarAffineFunction() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + MOI.set(model, MOI.VariableName(), x, "x") + f = MOI.ScalarAffineFunction( + MOI.ScalarAffineTerm.([-1.2, 1.3], [x, x]), + 1.4, + ) + @test MOI._to_string(PLAIN, model, f) == "1.4 - 1.2 x + 1.3 x" + @test MOI._to_string(LATEX, model, f) == "1.4 - 1.2 x + 1.3 x" +end + +function test_ScalarQuadraticTerm() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + y = MOI.add_variable(model) + MOI.set(model, MOI.VariableName(), x, "x") + MOI.set(model, MOI.VariableName(), y, "y") + term = MOI.ScalarQuadraticTerm(-1.2, x, x) + @test MOI._to_string(PLAIN, model, term) == " - 1.2 x²" + @test MOI._to_string(LATEX, model, term) == " - 1.2 x^2" + term = MOI.ScalarQuadraticTerm(1.2, x, y) + @test MOI._to_string(PLAIN, model, term) == " + 1.2 x*y" + @test MOI._to_string(LATEX, model, term) == " + 1.2 x\\times y" +end + +function test_ScalarQuadraticFunction() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + y = MOI.add_variable(model) + MOI.set(model, MOI.VariableName(), x, "x") + MOI.set(model, MOI.VariableName(), y, "y") + f = MOI.ScalarQuadraticFunction( + MOI.ScalarAffineTerm.([-1.2, 1.3], [x, x]), + MOI.ScalarQuadraticTerm.([0.5, 0.6], [x, x], [x, y]), + 1.4, + ) + @test MOI._to_string(PLAIN, model, f) == + "1.4 - 1.2 x + 1.3 x + 0.5 x² + 0.6 x*y" + @test MOI._to_string(LATEX, model, f) == + "1.4 - 1.2 x + 1.3 x + 0.5 x^2 + 0.6 x\\times y" +end + +function test_VectorOfVariables() + model = MOI.Utilities.Model{Float64}() + x = MOI.add_variable(model) + y = MOI.add_variable(model) + MOI.set(model, MOI.VariableName(), x, "x") + MOI.set(model, MOI.VariableName(), y, "y") + f = MOI.VectorOfVariables([x, y]) + @test MOI._to_string(PLAIN, model, f) == "┌ ┐\n│x│\n│y│\n└ ┘" + @test MOI._to_string(LATEX, model, f) == + "\\begin{bmatrix}\nx\\\\\ny\\end{bmatrix}" +end + +function test_LessThan() + s = MOI.LessThan(1.2) + @test MOI._to_string(PLAIN, s) == "<= 1.2" + @test MOI._to_string(LATEX, s) == "\\le 1.2" +end + +function test_GreaterThan() + s = MOI.GreaterThan(1.2) + @test MOI._to_string(PLAIN, s) == ">= 1.2" + @test MOI._to_string(LATEX, s) == "\\ge 1.2" +end + +function test_EqualTo() + s = MOI.EqualTo(1.2) + @test MOI._to_string(PLAIN, s) == "== 1.2" + @test MOI._to_string(LATEX, s) == "= 1.2" +end + +function test_Interval() + s = MOI.Interval(1.2, 1.3) + @test MOI._to_string(PLAIN, s) == "∈ [1.2, 1.3]" + @test MOI._to_string(LATEX, s) == "\\in \\[1.2, 1.3\\]" +end + +function test_ZeroOne() + s = MOI.ZeroOne() + @test MOI._to_string(PLAIN, s) == "∈ {0, 1}" + @test MOI._to_string(LATEX, s) == "\\in \\{0, 1\\}" +end + +function test_Integer() + s = MOI.Integer() + @test MOI._to_string(PLAIN, s) == "∈ ℤ" + @test MOI._to_string(LATEX, s) == "\\in \\mathbb{Z}" +end + +function test_ExponentialCone() + s = MOI.ExponentialCone() + @test MOI._to_string(PLAIN, s) == "∈ ExponentialCone()" + @test MOI._to_string(LATEX, s) == "\\in \\text{ExponentialCone()}" +end + +function test_feasibility() + model = MOI.Utilities.Model{Float64}() + @test sprint(print, model) == "Feasibility\n\nSubject to:\n" + @test sprint(print, MOI.latex_formulation(model)) == raw""" + $$ \begin{aligned} + \text{feasibility}\\ + \text{Subject to} \quad\end{aligned} $$""" +end + +function test_min() + model = MOI.Utilities.Model{Float64}() + MOI.Utilities.loadfromstring!(model, "variables: x\nminobjective: x") + @test sprint(print, model) == """ + Minimize SingleVariable: + x + + Subject to: + """ + @test sprint(print, MOI.latex_formulation(model)) == raw""" + $$ \begin{aligned} + \min\quad & x\\ + \text{Subject to} \quad\end{aligned} $$""" +end + +function test_max() + model = MOI.Utilities.Model{Float64}() + MOI.Utilities.loadfromstring!(model, "variables: x\nmaxobjective: x") + @test sprint(print, model) == """ + Maximize SingleVariable: + x + + Subject to: + """ + @test sprint(print, MOI.latex_formulation(model)) == raw""" + $$ \begin{aligned} + \max\quad & x\\ + \text{Subject to} \quad\end{aligned} $$""" +end + +function test_model() + model = MOI.Utilities.Model{Float64}() + MOI.Utilities.loadfromstring!( + model, + """ + variables: x, y, z + minobjective: x + 2 + 3.1*y + -1.2*z + c1: x >= 0.1 + c2: y in ZeroOne() + c2: z in Integer() + c3: [x, y] in SecondOrderCone(2) + c4: [1, x, y] in SecondOrderCone(2) + c4: [1.0 * x * x, y, 1] in ExponentialCone() + c4: [1, 1.0 * x * x, y] in ExponentialCone() + c2: x in ZeroOne() + c5: 2.0 * x * x + y + -1 * z <= 1.0 + c5: x + x >= 1.0 + c5: x + x in Interval(1.0, 2.0) + c5: x + -1 * y == 0.0 + """, + ) + @test sprint(print, model) == """ + Minimize ScalarAffineFunction{Float64}: + 2.0 + 1.0 x + 3.1 y - 1.2 z + + Subject to: + + ScalarAffineFunction{Float64}-in-EqualTo{Float64} + 0.0 + 1.0 x - 1.0 y == 0.0 + + ScalarAffineFunction{Float64}-in-GreaterThan{Float64} + 0.0 + 2.0 x >= 1.0 + + ScalarAffineFunction{Float64}-in-Interval{Float64} + 0.0 + 2.0 x ∈ [1.0, 2.0] + + ScalarQuadraticFunction{Float64}-in-LessThan{Float64} + 0.0 + 1.0 y - 1.0 z + 4.0 x² <= 1.0 + + VectorOfVariables-in-SecondOrderCone + ┌ ┐ + │x│ + │y│ + └ ┘ ∈ SecondOrderCone(2) + + VectorAffineFunction{Float64}-in-SecondOrderCone + ┌ ┐ + │1.0 │ + │0.0 + 1.0 x│ + │0.0 + 1.0 y│ + └ ┘ ∈ SecondOrderCone(2) + + VectorQuadraticFunction{Float64}-in-ExponentialCone + ┌ ┐ + │0.0 + 2.0 x²│ + │0.0 + 1.0 y │ + │1.0 │ + └ ┘ ∈ ExponentialCone() + ┌ ┐ + │1.0 │ + │0.0 + 2.0 x²│ + │0.0 + 1.0 y │ + └ ┘ ∈ ExponentialCone() + + SingleVariable-in-GreaterThan{Float64} + x >= 0.1 + + SingleVariable-in-Integer + z ∈ ℤ + + SingleVariable-in-ZeroOne + x ∈ {0, 1} + y ∈ {0, 1} + """ +end + +function test_latex() + model = MOI.Utilities.Model{Float64}() + MOI.Utilities.loadfromstring!( + model, + """ + variables: x, y, z + minobjective: x + 2 + 3.1*y + -1.2*z + c1: x >= 0.1 + c2: y in ZeroOne() + c2: z in Integer() + c3: [x, y] in SecondOrderCone(2) + c4: [1, x, y] in SecondOrderCone(2) + c4: [1.0 * x * x, y, 1] in ExponentialCone() + c4: [1, 1.0 * x * x, y] in ExponentialCone() + c2: x in ZeroOne() + c5: 2.0 * x * x + y + -1 * z <= 1.0 + c5: x + x >= 1.0 + c5: x + x in Interval(1.0, 2.0) + c5: x + -1 * y == 0.0 + """, + ) + evaluated = + sprint(io -> show(io, MIME("text/latex"), MOI.latex_formulation(model))) + @test evaluated == raw""" + $$ \begin{aligned} + \min\quad & 2.0 + 1.0 x + 3.1 y - 1.2 z\\ + \text{Subject to} \quadMathOptInterface.ScalarAffineFunction{Float64}-in-MathOptInterface.EqualTo{Float64} + & 0.0 + 1.0 x - 1.0 y = 0.0\\ + MathOptInterface.ScalarAffineFunction{Float64}-in-MathOptInterface.GreaterThan{Float64} + & 0.0 + 2.0 x \ge 1.0\\ + MathOptInterface.ScalarAffineFunction{Float64}-in-MathOptInterface.Interval{Float64} + & 0.0 + 2.0 x \in \[1.0, 2.0\]\\ + MathOptInterface.ScalarQuadraticFunction{Float64}-in-MathOptInterface.LessThan{Float64} + & 0.0 + 1.0 y - 1.0 z + 4.0 x^2 \le 1.0\\ + MathOptInterface.VectorOfVariables-in-MathOptInterface.SecondOrderCone + & \begin{bmatrix} + x\\ + y\end{bmatrix} \in \text{SecondOrderCone(2)}\\ + MathOptInterface.VectorAffineFunction{Float64}-in-MathOptInterface.SecondOrderCone + & \begin{bmatrix} + 1.0\\ + 0.0 + 1.0 x\\ + 0.0 + 1.0 y\end{bmatrix} \in \text{SecondOrderCone(2)}\\ + MathOptInterface.VectorQuadraticFunction{Float64}-in-MathOptInterface.ExponentialCone + & \begin{bmatrix} + 0.0 + 2.0 x^2\\ + 0.0 + 1.0 y\\ + 1.0\end{bmatrix} \in \text{ExponentialCone()}\\ + & \begin{bmatrix} + 1.0\\ + 0.0 + 2.0 x^2\\ + 0.0 + 1.0 y\end{bmatrix} \in \text{ExponentialCone()}\\ + MathOptInterface.SingleVariable-in-MathOptInterface.GreaterThan{Float64} + & x \ge 0.1\\ + MathOptInterface.SingleVariable-in-MathOptInterface.Integer + & z \in \mathbb{Z}\\ + MathOptInterface.SingleVariable-in-MathOptInterface.ZeroOne + & x \in \{0, 1\}\\ + & y \in \{0, 1\}\\ + \end{aligned} $$""" +end + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end +end + +end + +TestPrint.runtests() From f5931130af6674ac56be85569b51568fae28315a Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 19 Apr 2021 15:14:19 +1200 Subject: [PATCH 02/11] Add docs and move to Utilities --- docs/src/submodules/Utilities/overview.md | 46 ++++++ docs/src/submodules/Utilities/reference.md | 6 + src/MathOptInterface.jl | 1 - src/Utilities/Utilities.jl | 1 + src/{ => Utilities}/print.jl | 137 ++++++++------- test/Utilities/Utilities.jl | 53 +----- test/{ => Utilities}/print.jl | 184 +++++++++++---------- 7 files changed, 222 insertions(+), 206 deletions(-) rename src/{ => Utilities}/print.jl (61%) rename test/{ => Utilities}/print.jl (55%) diff --git a/docs/src/submodules/Utilities/overview.md b/docs/src/submodules/Utilities/overview.md index c643e9a432..6df1c06b65 100644 --- a/docs/src/submodules/Utilities/overview.md +++ b/docs/src/submodules/Utilities/overview.md @@ -265,6 +265,52 @@ with model cache MOIU.GenericModel{Float64,MOIU.ModelFunctionConstraints{Float64 with optimizer MOIU.GenericOptimizer{Float64,MOIU.VectorOfConstraints{MOI.VectorAffineFunction{Float64},MOI.Complements}} ``` +## Printing + +Use `print` to print the formulation of the model. +```jldoctest utilities_print +julia> model = MOI.Utilities.Model{Float64}(); + +julia> x = MOI.add_variable(model) +MathOptInterface.VariableIndex(1) + +julia> MOI.set(model, MOI.VariableName(), x, "x_var") + +julia> f = MOI.SingleVariable(x) +MathOptInterface.SingleVariable(MathOptInterface.VariableIndex(1)) + +julia> MOI.add_constraint(model, f, MOI.ZeroOne()) +MathOptInterface.ConstraintIndex{MathOptInterface.SingleVariable, MathOptInterface.ZeroOne}(1) + +julia> MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + +julia> MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + +julia> print(model) +Maximize SingleVariable: + x_var + +Subject to: + +SingleVariable-in-ZeroOne + x_var ∈ {0, 1} +``` + +Use [`latex_formulation`](@Ref) to display the model in LaTeX form: +```jldoctest utilities_print +julia> MOI.Utilities.latex_formulation(model) +$$ \begin{aligned} +\max\quad & x\_var \\ +\text{Subject to} \\ + & \text{SingleVariable-in-ZeroOne} \\ + & x\_var \in \{0, 1\} \\ +\end{aligned} $$ +``` + +!!! tip + In IJulia, calling `print` or ending a cell with [`latex_formulation`](@ref) + will render the model in LaTex. + ## Copy utilities !!! info diff --git a/docs/src/submodules/Utilities/reference.md b/docs/src/submodules/Utilities/reference.md index f02f54e408..9889045153 100644 --- a/docs/src/submodules/Utilities/reference.md +++ b/docs/src/submodules/Utilities/reference.md @@ -47,6 +47,12 @@ Utilities.state Utilities.mode ``` +## Printing + +```@docs +Utilities.latex_formulation +``` + ## Copy utilities The following utilities can be used to implement [`copy_to`](@ref). See diff --git a/src/MathOptInterface.jl b/src/MathOptInterface.jl index 251da0d3c1..03015f7558 100644 --- a/src/MathOptInterface.jl +++ b/src/MathOptInterface.jl @@ -139,7 +139,6 @@ include("constraints.jl") include("modifications.jl") include("variables.jl") include("nlp.jl") -include("print.jl") if VERSION > v"1.4.2" include("precompile.jl") diff --git a/src/Utilities/Utilities.jl b/src/Utilities/Utilities.jl index 5dcee28f15..b689f2f0d1 100644 --- a/src/Utilities/Utilities.jl +++ b/src/Utilities/Utilities.jl @@ -64,6 +64,7 @@ include("parser.jl") include("mockoptimizer.jl") include("cachingoptimizer.jl") include("universalfallback.jl") +include("print.jl") include("lazy_iterators.jl") diff --git a/src/print.jl b/src/Utilities/print.jl similarity index 61% rename from src/print.jl rename to src/Utilities/print.jl index 41de755b8d..dfd7446c66 100644 --- a/src/print.jl +++ b/src/Utilities/print.jl @@ -45,13 +45,13 @@ end # Functions #------------------------------------------------------------------------ -function _to_string(::MIME, model::ModelLike, v::VariableIndex) - var_name = get(model, VariableName(), v) +function _to_string(::MIME, model::MOI.ModelLike, v::MOI.VariableIndex) + var_name = MOI.get(model, MOI.VariableName(), v) return isempty(var_name) ? "noname" : var_name end -function _to_string(::MIME"text/latex", model::ModelLike, v::VariableIndex) - var_name = get(model, VariableName(), v) +function _to_string(::MIME"text/latex", model::MOI.ModelLike, v::MOI.VariableIndex) + var_name = MOI.get(model, MOI.VariableName(), v) if isempty(var_name) return "noname" end @@ -70,11 +70,11 @@ function _to_string(::MIME"text/latex", model::ModelLike, v::VariableIndex) return var_name end -function _to_string(mime, model::ModelLike, f::SingleVariable) +function _to_string(mime::MIME, model::MOI.ModelLike, f::MOI.SingleVariable) return _to_string(mime, model, f.variable) end -function _to_string(mime, model::ModelLike, term::ScalarAffineTerm) +function _to_string(mime::MIME, model::MOI.ModelLike, term::MOI.ScalarAffineTerm) name = _to_string(mime, model, term.variable_index) if term.coefficient < 0 return string(" - ", string(-term.coefficient), " ", name) @@ -83,7 +83,7 @@ function _to_string(mime, model::ModelLike, term::ScalarAffineTerm) end end -function _to_string(mime, model::ModelLike, f::ScalarAffineFunction) +function _to_string(mime::MIME, model::MOI.ModelLike, f::MOI.ScalarAffineFunction) s = string(f.constant) for term in f.terms s *= _to_string(mime, model, term) @@ -91,7 +91,7 @@ function _to_string(mime, model::ModelLike, f::ScalarAffineFunction) return s end -function _to_string(mime, model::ModelLike, term::ScalarQuadraticTerm) +function _to_string(mime::MIME, model::MOI.ModelLike, term::MOI.ScalarQuadraticTerm) name_1 = _to_string(mime, model, term.variable_index_1) name_2 = _to_string(mime, model, term.variable_index_2) name = if term.variable_index_1 == term.variable_index_2 @@ -106,7 +106,7 @@ function _to_string(mime, model::ModelLike, term::ScalarQuadraticTerm) end end -function _to_string(mime, model::ModelLike, f::ScalarQuadraticFunction) +function _to_string(mime::MIME, model::MOI.ModelLike, f::MOI.ScalarQuadraticFunction) s = string(f.constant) for term in f.affine_terms s *= _to_string(mime, model, term) @@ -117,8 +117,8 @@ function _to_string(mime, model::ModelLike, f::ScalarQuadraticFunction) return s end -function _to_string(mime, model::ModelLike, f::AbstractVectorFunction) - rows = map(fi -> _to_string(mime, model, fi), Utilities.eachscalar(f)) +function _to_string(mime::MIME, model::MOI.ModelLike, f::MOI.AbstractVectorFunction) + rows = map(fi -> _to_string(mime, model, fi), eachscalar(f)) max_length = maximum(length.(rows)) s = join(map(r -> string("│", rpad(r, max_length), "│"), rows), '\n') return string( @@ -134,15 +134,12 @@ end function _to_string( mime::MIME"text/latex", - model::ModelLike, - f::AbstractVectorFunction, + model::MOI.ModelLike, + f::MOI.AbstractVectorFunction, ) return string( "\\begin{bmatrix}\n", - join( - map(fi -> _to_string(mime, model, fi), Utilities.eachscalar(f)), - "\\\\\n", - ), + join(map(fi -> _to_string(mime, model, fi), eachscalar(f)), "\\\\\n"), "\\end{bmatrix}", ) end @@ -151,45 +148,45 @@ end # Sets #------------------------------------------------------------------------ -function _to_string(mime, set::LessThan) +function _to_string(mime::MIME, set::MOI.LessThan) return string(_to_string(mime, :leq), " ", set.upper) end -_to_string(::MIME"text/latex", set::LessThan) = "\\le $(set.upper)" +_to_string(::MIME"text/latex", set::MOI.LessThan) = "\\le $(set.upper)" -function _to_string(mime, set::GreaterThan) +function _to_string(mime::MIME, set::MOI.GreaterThan) return string(_to_string(mime, :geq), " ", set.lower) end -_to_string(::MIME"text/latex", set::GreaterThan) = "\\ge $(set.lower)" +_to_string(::MIME"text/latex", set::MOI.GreaterThan) = "\\ge $(set.lower)" -function _to_string(mime, set::EqualTo) +function _to_string(mime::MIME, set::MOI.EqualTo) return string(_to_string(mime, :eq), " ", set.value) end -_to_string(::MIME"text/latex", set::EqualTo) = "= $(set.value)" +_to_string(::MIME"text/latex", set::MOI.EqualTo) = "= $(set.value)" -function _to_string(mime, set::Interval) +function _to_string(mime::MIME, set::MOI.Interval) return string(_to_string(mime, :in), " [", set.lower, ", ", set.upper, "]") end -function _to_string(::MIME"text/latex", set::Interval) +function _to_string(::MIME"text/latex", set::MOI.Interval) return "\\in \\[$(set.lower), $(set.upper)\\]" end -_to_string(::MIME, ::ZeroOne) = "∈ {0, 1}" +_to_string(mime::MIME, ::MOI.ZeroOne) = string(_to_string(mime, :in), " {0, 1}") -_to_string(::MIME"text/latex", ::ZeroOne) = "\\in \\{0, 1\\}" +_to_string(::MIME"text/latex", ::MOI.ZeroOne) = "\\in \\{0, 1\\}" -_to_string(::MIME, ::Integer) = "∈ ℤ" +_to_string(mime::MIME, ::MOI.Integer) = string(_to_string(mime, :in), " ℤ") -_to_string(::MIME"text/latex", ::Integer) = "\\in \\mathbb{Z}" +_to_string(::MIME"text/latex", ::MOI.Integer) = "\\in \\mathbb{Z}" -function _to_string(mime, set::AbstractSet) +function _to_string(mime::MIME, set::MOI.AbstractSet) return string(_to_string(mime, :in), " ", _drop_moi(set)) end -function _to_string(::MIME"text/latex", set::AbstractSet) +function _to_string(::MIME"text/latex", set::MOI.AbstractSet) set_str = replace(replace(_drop_moi(set), "{" => "\\{"), "}" => "\\}") return string("\\in \\text{", set_str, "}") end @@ -198,40 +195,40 @@ end # Constraints #------------------------------------------------------------------------ -function _to_string(mime::MIME, model::ModelLike, cref::ConstraintIndex) - f = get(model, ConstraintFunction(), cref) - s = get(model, ConstraintSet(), cref) +function _to_string(mime::MIME, model::MOI.ModelLike, cref::MOI.ConstraintIndex) + f = MOI.get(model, MOI.ConstraintFunction(), cref) + s = MOI.get(model, MOI.ConstraintSet(), cref) return string(_to_string(mime, model, f), " ", _to_string(mime, s)) end #------------------------------------------------------------------------ -# ModelLike +# MOI.ModelLike #------------------------------------------------------------------------ """ - _print_model(io::IO, model::ModelLike, mime::MIME"text/plain") + _print_model(io::IO, mime::MIME"text/plain", model::MOI.ModelLike) Print a plain-text formulation of `model` to `io`. """ -function _print_model(io::IO, model::ModelLike, mime::MIME"text/plain") - sense = get(model, ObjectiveSense()) - if sense == MAX_SENSE - F = get(model, ObjectiveFunctionType()) +function _print_model(io::IO, mime::MIME"text/plain", model::MOI.ModelLike) + sense = MOI.get(model, MOI.ObjectiveSense()) + if sense == MOI.MAX_SENSE + F = MOI.get(model, MOI.ObjectiveFunctionType()) println(io, "Maximize $(_drop_moi(F)):") - f = get(model, ObjectiveFunction{F}()) + f = MOI.get(model, MOI.ObjectiveFunction{F}()) println(io, " ", _to_string(mime, model, f)) - elseif sense == MIN_SENSE - F = get(model, ObjectiveFunctionType()) + elseif sense == MOI.MIN_SENSE + F = MOI.get(model, MOI.ObjectiveFunctionType()) println(io, "Minimize $(_drop_moi(F)):") - f = get(model, ObjectiveFunction{F}()) + f = MOI.get(model, MOI.ObjectiveFunction{F}()) println(io, " ", _to_string(mime, model, f)) else println(io, "Feasibility") end println(io, "\nSubject to:") - for (F, S) in get(model, ListOfConstraints()) + for (F, S) in MOI.get(model, MOI.ListOfConstraints()) println(io, "\n$(_drop_moi(F))-in-$(_drop_moi(S))") - for cref in get(model, ListOfConstraintIndices{F,S}()) + for cref in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) s = _to_string(mime, model, cref) println(io, " ", replace(s, '\n' => "\n ")) end @@ -240,31 +237,31 @@ function _print_model(io::IO, model::ModelLike, mime::MIME"text/plain") end """ - _print_model(io::IO, model::ModelLike, mime::MIME"text/latex") + _print_model(io::IO, mime::MIME"text/latex", model::MOI.ModelLike) Print a LaTeX formulation of `model` to `io`. """ -function _print_model(io::IO, model::ModelLike, mime::MIME"text/latex") +function _print_model(io::IO, mime::MIME"text/latex", model::MOI.ModelLike) println(io, "\$\$ \\begin{aligned}") - sense = get(model, ObjectiveSense()) - if sense == MAX_SENSE + sense = MOI.get(model, MOI.ObjectiveSense()) + if sense == MOI.MAX_SENSE print(io, "\\max\\quad & ") - F = get(model, ObjectiveFunctionType()) - f = get(model, ObjectiveFunction{F}()) - println(io, _to_string(mime, model, f), "\\\\") - elseif sense == MIN_SENSE + F = MOI.get(model, MOI.ObjectiveFunctionType()) + f = MOI.get(model, MOI.ObjectiveFunction{F}()) + println(io, _to_string(mime, model, f), " \\\\") + elseif sense == MOI.MIN_SENSE print(io, "\\min\\quad & ") - F = get(model, ObjectiveFunctionType()) - f = get(model, ObjectiveFunction{F}()) - println(io, _to_string(mime, model, f), "\\\\") + F = MOI.get(model, MOI.ObjectiveFunctionType()) + f = MOI.get(model, MOI.ObjectiveFunction{F}()) + println(io, _to_string(mime, model, f), " \\\\") else println(io, "\\text{feasibility}\\\\") end - print(io, "\\text{Subject to} \\quad") - for (F, S) in get(model, ListOfConstraints()) - println(io, "$F-in-$S") - for cref in get(model, ListOfConstraintIndices{F,S}()) - println(io, " & ", _to_string(mime, model, cref), "\\\\") + println(io, "\\text{Subject to}\\\\") + for (F, S) in MOI.get(model, MOI.ListOfConstraints()) + println(io, " & \\text{$(_drop_moi(F))-in-$(_drop_moi(S))} \\\\") + for cref in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + println(io, " & ", _to_string(mime, model, cref), " \\\\") end end return print(io, "\\end{aligned} \$\$") @@ -274,12 +271,12 @@ end # Latex #------------------------------------------------------------------------ -struct _LatexModel{T<:ModelLike} +struct _LatexModel{T<:MOI.ModelLike} model::T end """ - latex_formulation(model::ModelLike) + latex_formulation(model::MOI.ModelLike) Wrap `model` in a type so that it can be pretty-printed as `text/latex` in a notebook like IJulia, or in Documenter. @@ -288,24 +285,24 @@ To render the model, end the cell with `latex_formulation(model)`, or call `display(latex_formulation(model))` in to force the display of the model from inside a function. """ -latex_formulation(model::ModelLike) = _LatexModel(model) +latex_formulation(model::MOI.ModelLike) = _LatexModel(model) function Base.show(io::IO, model::_LatexModel) - return _print_model(io, model.model, MIME("text/latex")) + return _print_model(io, MIME("text/latex"), model.model) end Base.show(io::IO, ::MIME"text/latex", model::_LatexModel) = show(io, model) -function Base.print(model::ModelLike) +function Base.print(model::MOI.ModelLike) for d in Base.Multimedia.displays if Base.Multimedia.displayable(d, "text/latex") && startswith("$(typeof(d))", "IJulia.") return display(d, "text/latex", latex_formulation(model)) end end - return _print_model(stdout, model, MIME("text/plain")) + return _print_model(stdout, MIME("text/plain"), model) end -function Base.print(io::IO, model::ModelLike) - return _print_model(io, model, MIME("text/plain")) +function Base.print(io::IO, model::MOI.ModelLike) + return _print_model(io, MIME("text/plain"), model) end diff --git a/test/Utilities/Utilities.jl b/test/Utilities/Utilities.jl index df0037a83a..e6c94881e3 100644 --- a/test/Utilities/Utilities.jl +++ b/test/Utilities/Utilities.jl @@ -1,49 +1,10 @@ using Test -@testset "Functions" begin - include("functions.jl") -end -@testset "Mutable Arithmetics" begin - include("mutable_arithmetics.jl") -end -@testset "Sets" begin - include("sets.jl") -end -@testset "Constraints" begin - include("constraints.jl") -end -@testset "Variables" begin - include("variables.jl") -end -@testset "Model" begin - include("model.jl") -end -@testset "Universal Fallback" begin - include("universalfallback.jl") -end -@testset "Parser" begin - include("parser.jl") -end -@testset "Mock Optimizer" begin - include("mockoptimizer.jl") -end -@testset "Caching Optimizer" begin - include("cachingoptimizer.jl") -end -@testset "Copy" begin - include("copy.jl") -end - -@testset "CleverDicts" begin - include("CleverDicts.jl") -end -@testset "DoubleDicts" begin - include("DoubleDicts.jl") -end -@testset "Lazy iterators" begin - include("lazy_iterators.jl") -end - -@testset "Print with acronym" begin - include("print_with_acronym.jl") +for file in readdir(@__DIR__) + if file in ["Utilities.jl"] + continue + end + @testset "$(file)" begin + include(file) + end end diff --git a/test/print.jl b/test/Utilities/print.jl similarity index 55% rename from test/print.jl rename to test/Utilities/print.jl index 89f7a7802d..a737b8c748 100644 --- a/test/print.jl +++ b/test/Utilities/print.jl @@ -2,77 +2,79 @@ module TestPrint using MathOptInterface const MOI = MathOptInterface +const MOIU = MOI.Utilities using Test const LATEX = MIME("text/latex") const PLAIN = MIME("text/plain") +const IN = Sys.iswindows() ? "in" : "∈" function test_nonname_variable() - model = MOI.Utilities.Model{Float64}() + model = MOIU.Model{Float64}() x = MOI.add_variable(model) - @test MOI._to_string(PLAIN, model, x) == "noname" - @test MOI._to_string(LATEX, model, x) == "noname" + @test MOIU._to_string(PLAIN, model, x) == "noname" + @test MOIU._to_string(LATEX, model, x) == "noname" end function test_variable() - model = MOI.Utilities.Model{Float64}() + model = MOIU.Model{Float64}() x = MOI.add_variable(model) for (name, latex_name) in [("x", "x"), ("x_y", "x\\_y"), ("x^2", "x\\^2"), ("x[a,b]", "x_{a,b}")] MOI.set(model, MOI.VariableName(), x, name) - @test MOI._to_string(PLAIN, model, x) == name - @test MOI._to_string(LATEX, model, x) == latex_name + @test MOIU._to_string(PLAIN, model, x) == name + @test MOIU._to_string(LATEX, model, x) == latex_name end end function test_single_variable() - model = MOI.Utilities.Model{Float64}() + model = MOIU.Model{Float64}() x = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") f = MOI.SingleVariable(x) - @test MOI._to_string(PLAIN, model, f) == "x" - @test MOI._to_string(LATEX, model, f) == "x" + @test MOIU._to_string(PLAIN, model, f) == "x" + @test MOIU._to_string(LATEX, model, f) == "x" end function test_ScalarAffineTerm() - model = MOI.Utilities.Model{Float64}() + model = MOIU.Model{Float64}() x = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") - @test MOI._to_string(PLAIN, model, MOI.ScalarAffineTerm(-1.2, x)) == + @test MOIU._to_string(PLAIN, model, MOI.ScalarAffineTerm(-1.2, x)) == " - 1.2 x" - @test MOI._to_string(LATEX, model, MOI.ScalarAffineTerm(1.2, x)) == + @test MOIU._to_string(LATEX, model, MOI.ScalarAffineTerm(1.2, x)) == " + 1.2 x" end function test_ScalarAffineFunction() - model = MOI.Utilities.Model{Float64}() + model = MOIU.Model{Float64}() x = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") f = MOI.ScalarAffineFunction( MOI.ScalarAffineTerm.([-1.2, 1.3], [x, x]), 1.4, ) - @test MOI._to_string(PLAIN, model, f) == "1.4 - 1.2 x + 1.3 x" - @test MOI._to_string(LATEX, model, f) == "1.4 - 1.2 x + 1.3 x" + @test MOIU._to_string(PLAIN, model, f) == "1.4 - 1.2 x + 1.3 x" + @test MOIU._to_string(LATEX, model, f) == "1.4 - 1.2 x + 1.3 x" end function test_ScalarQuadraticTerm() - model = MOI.Utilities.Model{Float64}() + model = MOIU.Model{Float64}() x = MOI.add_variable(model) y = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") MOI.set(model, MOI.VariableName(), y, "y") term = MOI.ScalarQuadraticTerm(-1.2, x, x) - @test MOI._to_string(PLAIN, model, term) == " - 1.2 x²" - @test MOI._to_string(LATEX, model, term) == " - 1.2 x^2" + @test MOIU._to_string(PLAIN, model, term) == " - 1.2 x²" + @test MOIU._to_string(LATEX, model, term) == " - 1.2 x^2" term = MOI.ScalarQuadraticTerm(1.2, x, y) - @test MOI._to_string(PLAIN, model, term) == " + 1.2 x*y" - @test MOI._to_string(LATEX, model, term) == " + 1.2 x\\times y" + @test MOIU._to_string(PLAIN, model, term) == " + 1.2 x*y" + @test MOIU._to_string(LATEX, model, term) == " + 1.2 x\\times y" end function test_ScalarQuadraticFunction() - model = MOI.Utilities.Model{Float64}() + model = MOIU.Model{Float64}() x = MOI.add_variable(model) y = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") @@ -82,108 +84,111 @@ function test_ScalarQuadraticFunction() MOI.ScalarQuadraticTerm.([0.5, 0.6], [x, x], [x, y]), 1.4, ) - @test MOI._to_string(PLAIN, model, f) == + @test MOIU._to_string(PLAIN, model, f) == "1.4 - 1.2 x + 1.3 x + 0.5 x² + 0.6 x*y" - @test MOI._to_string(LATEX, model, f) == + @test MOIU._to_string(LATEX, model, f) == "1.4 - 1.2 x + 1.3 x + 0.5 x^2 + 0.6 x\\times y" end function test_VectorOfVariables() - model = MOI.Utilities.Model{Float64}() + model = MOIU.Model{Float64}() x = MOI.add_variable(model) y = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") MOI.set(model, MOI.VariableName(), y, "y") f = MOI.VectorOfVariables([x, y]) - @test MOI._to_string(PLAIN, model, f) == "┌ ┐\n│x│\n│y│\n└ ┘" - @test MOI._to_string(LATEX, model, f) == + @test MOIU._to_string(PLAIN, model, f) == "┌ ┐\n│x│\n│y│\n└ ┘" + @test MOIU._to_string(LATEX, model, f) == "\\begin{bmatrix}\nx\\\\\ny\\end{bmatrix}" end function test_LessThan() s = MOI.LessThan(1.2) - @test MOI._to_string(PLAIN, s) == "<= 1.2" - @test MOI._to_string(LATEX, s) == "\\le 1.2" + @test MOIU._to_string(PLAIN, s) == "<= 1.2" + @test MOIU._to_string(LATEX, s) == "\\le 1.2" end function test_GreaterThan() s = MOI.GreaterThan(1.2) - @test MOI._to_string(PLAIN, s) == ">= 1.2" - @test MOI._to_string(LATEX, s) == "\\ge 1.2" + @test MOIU._to_string(PLAIN, s) == ">= 1.2" + @test MOIU._to_string(LATEX, s) == "\\ge 1.2" end function test_EqualTo() s = MOI.EqualTo(1.2) - @test MOI._to_string(PLAIN, s) == "== 1.2" - @test MOI._to_string(LATEX, s) == "= 1.2" + @test MOIU._to_string(PLAIN, s) == "== 1.2" + @test MOIU._to_string(LATEX, s) == "= 1.2" end function test_Interval() s = MOI.Interval(1.2, 1.3) - @test MOI._to_string(PLAIN, s) == "∈ [1.2, 1.3]" - @test MOI._to_string(LATEX, s) == "\\in \\[1.2, 1.3\\]" + @test MOIU._to_string(PLAIN, s) == "$(IN) [1.2, 1.3]" + @test MOIU._to_string(LATEX, s) == "\\in \\[1.2, 1.3\\]" end function test_ZeroOne() s = MOI.ZeroOne() - @test MOI._to_string(PLAIN, s) == "∈ {0, 1}" - @test MOI._to_string(LATEX, s) == "\\in \\{0, 1\\}" + @test MOIU._to_string(PLAIN, s) == "$(IN) {0, 1}" + @test MOIU._to_string(LATEX, s) == "\\in \\{0, 1\\}" end function test_Integer() s = MOI.Integer() - @test MOI._to_string(PLAIN, s) == "∈ ℤ" - @test MOI._to_string(LATEX, s) == "\\in \\mathbb{Z}" + @test MOIU._to_string(PLAIN, s) == "$(IN) ℤ" + @test MOIU._to_string(LATEX, s) == "\\in \\mathbb{Z}" end function test_ExponentialCone() s = MOI.ExponentialCone() - @test MOI._to_string(PLAIN, s) == "∈ ExponentialCone()" - @test MOI._to_string(LATEX, s) == "\\in \\text{ExponentialCone()}" + @test MOIU._to_string(PLAIN, s) == "$(IN) ExponentialCone()" + @test MOIU._to_string(LATEX, s) == "\\in \\text{ExponentialCone()}" end function test_feasibility() - model = MOI.Utilities.Model{Float64}() + model = MOIU.Model{Float64}() @test sprint(print, model) == "Feasibility\n\nSubject to:\n" - @test sprint(print, MOI.latex_formulation(model)) == raw""" + @test sprint(print, MOIU.latex_formulation(model)) == raw""" $$ \begin{aligned} \text{feasibility}\\ - \text{Subject to} \quad\end{aligned} $$""" + \text{Subject to}\\ + \end{aligned} $$""" end function test_min() - model = MOI.Utilities.Model{Float64}() - MOI.Utilities.loadfromstring!(model, "variables: x\nminobjective: x") + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, "variables: x\nminobjective: x") @test sprint(print, model) == """ Minimize SingleVariable: x Subject to: """ - @test sprint(print, MOI.latex_formulation(model)) == raw""" + @test sprint(print, MOIU.latex_formulation(model)) == raw""" $$ \begin{aligned} - \min\quad & x\\ - \text{Subject to} \quad\end{aligned} $$""" + \min\quad & x \\ + \text{Subject to}\\ + \end{aligned} $$""" end function test_max() - model = MOI.Utilities.Model{Float64}() - MOI.Utilities.loadfromstring!(model, "variables: x\nmaxobjective: x") + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, "variables: x\nmaxobjective: x") @test sprint(print, model) == """ Maximize SingleVariable: x Subject to: """ - @test sprint(print, MOI.latex_formulation(model)) == raw""" + @test sprint(print, MOIU.latex_formulation(model)) == raw""" $$ \begin{aligned} - \max\quad & x\\ - \text{Subject to} \quad\end{aligned} $$""" + \max\quad & x \\ + \text{Subject to}\\ + \end{aligned} $$""" end function test_model() - model = MOI.Utilities.Model{Float64}() - MOI.Utilities.loadfromstring!( + model = MOIU.Model{Float64}() + MOIU.loadfromstring!( model, """ variables: x, y, z @@ -215,7 +220,7 @@ function test_model() 0.0 + 2.0 x >= 1.0 ScalarAffineFunction{Float64}-in-Interval{Float64} - 0.0 + 2.0 x ∈ [1.0, 2.0] + 0.0 + 2.0 x $(IN) [1.0, 2.0] ScalarQuadraticFunction{Float64}-in-LessThan{Float64} 0.0 + 1.0 y - 1.0 z + 4.0 x² <= 1.0 @@ -224,42 +229,42 @@ function test_model() ┌ ┐ │x│ │y│ - └ ┘ ∈ SecondOrderCone(2) + └ ┘ $(IN) SecondOrderCone(2) VectorAffineFunction{Float64}-in-SecondOrderCone ┌ ┐ │1.0 │ │0.0 + 1.0 x│ │0.0 + 1.0 y│ - └ ┘ ∈ SecondOrderCone(2) + └ ┘ $(IN) SecondOrderCone(2) VectorQuadraticFunction{Float64}-in-ExponentialCone ┌ ┐ │0.0 + 2.0 x²│ │0.0 + 1.0 y │ │1.0 │ - └ ┘ ∈ ExponentialCone() + └ ┘ $(IN) ExponentialCone() ┌ ┐ │1.0 │ │0.0 + 2.0 x²│ │0.0 + 1.0 y │ - └ ┘ ∈ ExponentialCone() + └ ┘ $(IN) ExponentialCone() SingleVariable-in-GreaterThan{Float64} x >= 0.1 SingleVariable-in-Integer - z ∈ ℤ + z $(IN) ℤ SingleVariable-in-ZeroOne - x ∈ {0, 1} - y ∈ {0, 1} + x $(IN) {0, 1} + y $(IN) {0, 1} """ end function test_latex() - model = MOI.Utilities.Model{Float64}() - MOI.Utilities.loadfromstring!( + model = MOIU.Model{Float64}() + MOIU.loadfromstring!( model, """ variables: x, y, z @@ -279,43 +284,44 @@ function test_latex() """, ) evaluated = - sprint(io -> show(io, MIME("text/latex"), MOI.latex_formulation(model))) + sprint(io -> show(io, MIME("text/latex"), MOIU.latex_formulation(model))) @test evaluated == raw""" $$ \begin{aligned} - \min\quad & 2.0 + 1.0 x + 3.1 y - 1.2 z\\ - \text{Subject to} \quadMathOptInterface.ScalarAffineFunction{Float64}-in-MathOptInterface.EqualTo{Float64} - & 0.0 + 1.0 x - 1.0 y = 0.0\\ - MathOptInterface.ScalarAffineFunction{Float64}-in-MathOptInterface.GreaterThan{Float64} - & 0.0 + 2.0 x \ge 1.0\\ - MathOptInterface.ScalarAffineFunction{Float64}-in-MathOptInterface.Interval{Float64} - & 0.0 + 2.0 x \in \[1.0, 2.0\]\\ - MathOptInterface.ScalarQuadraticFunction{Float64}-in-MathOptInterface.LessThan{Float64} - & 0.0 + 1.0 y - 1.0 z + 4.0 x^2 \le 1.0\\ - MathOptInterface.VectorOfVariables-in-MathOptInterface.SecondOrderCone + \min\quad & 2.0 + 1.0 x + 3.1 y - 1.2 z \\ + \text{Subject to}\\ + & \text{ScalarAffineFunction{Float64}-in-EqualTo{Float64}} \\ + & 0.0 + 1.0 x - 1.0 y = 0.0 \\ + & \text{ScalarAffineFunction{Float64}-in-GreaterThan{Float64}} \\ + & 0.0 + 2.0 x \ge 1.0 \\ + & \text{ScalarAffineFunction{Float64}-in-Interval{Float64}} \\ + & 0.0 + 2.0 x \in \[1.0, 2.0\] \\ + & \text{ScalarQuadraticFunction{Float64}-in-LessThan{Float64}} \\ + & 0.0 + 1.0 y - 1.0 z + 4.0 x^2 \le 1.0 \\ + & \text{VectorOfVariables-in-SecondOrderCone} \\ & \begin{bmatrix} x\\ - y\end{bmatrix} \in \text{SecondOrderCone(2)}\\ - MathOptInterface.VectorAffineFunction{Float64}-in-MathOptInterface.SecondOrderCone + y\end{bmatrix} \in \text{SecondOrderCone(2)} \\ + & \text{VectorAffineFunction{Float64}-in-SecondOrderCone} \\ & \begin{bmatrix} 1.0\\ 0.0 + 1.0 x\\ - 0.0 + 1.0 y\end{bmatrix} \in \text{SecondOrderCone(2)}\\ - MathOptInterface.VectorQuadraticFunction{Float64}-in-MathOptInterface.ExponentialCone + 0.0 + 1.0 y\end{bmatrix} \in \text{SecondOrderCone(2)} \\ + & \text{VectorQuadraticFunction{Float64}-in-ExponentialCone} \\ & \begin{bmatrix} 0.0 + 2.0 x^2\\ 0.0 + 1.0 y\\ - 1.0\end{bmatrix} \in \text{ExponentialCone()}\\ + 1.0\end{bmatrix} \in \text{ExponentialCone()} \\ & \begin{bmatrix} 1.0\\ 0.0 + 2.0 x^2\\ - 0.0 + 1.0 y\end{bmatrix} \in \text{ExponentialCone()}\\ - MathOptInterface.SingleVariable-in-MathOptInterface.GreaterThan{Float64} - & x \ge 0.1\\ - MathOptInterface.SingleVariable-in-MathOptInterface.Integer - & z \in \mathbb{Z}\\ - MathOptInterface.SingleVariable-in-MathOptInterface.ZeroOne - & x \in \{0, 1\}\\ - & y \in \{0, 1\}\\ + 0.0 + 1.0 y\end{bmatrix} \in \text{ExponentialCone()} \\ + & \text{SingleVariable-in-GreaterThan{Float64}} \\ + & x \ge 0.1 \\ + & \text{SingleVariable-in-Integer} \\ + & z \in \mathbb{Z} \\ + & \text{SingleVariable-in-ZeroOne} \\ + & x \in \{0, 1\} \\ + & y \in \{0, 1\} \\ \end{aligned} $$""" end From 839a56911b20aa177f1636406484f3563c4ff0d4 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 19 Apr 2021 16:12:32 +1200 Subject: [PATCH 03/11] Updates --- docs/src/submodules/Utilities/overview.md | 10 +- src/Utilities/print.jl | 202 +++++++++++++++++++--- test/Utilities/print.jl | 62 ++++++- 3 files changed, 239 insertions(+), 35 deletions(-) diff --git a/docs/src/submodules/Utilities/overview.md b/docs/src/submodules/Utilities/overview.md index 6df1c06b65..4b6f8c3199 100644 --- a/docs/src/submodules/Utilities/overview.md +++ b/docs/src/submodules/Utilities/overview.md @@ -280,7 +280,7 @@ julia> f = MOI.SingleVariable(x) MathOptInterface.SingleVariable(MathOptInterface.VariableIndex(1)) julia> MOI.add_constraint(model, f, MOI.ZeroOne()) -MathOptInterface.ConstraintIndex{MathOptInterface.SingleVariable, MathOptInterface.ZeroOne}(1) +MathOptInterface.ConstraintIndex{MathOptInterface.SingleVariable,MathOptInterface.ZeroOne}(1) julia> MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) @@ -296,20 +296,20 @@ SingleVariable-in-ZeroOne x_var ∈ {0, 1} ``` -Use [`latex_formulation`](@Ref) to display the model in LaTeX form: +Use [`Utilities.latex_formulation`](@Ref) to display the model in LaTeX form: ```jldoctest utilities_print julia> MOI.Utilities.latex_formulation(model) $$ \begin{aligned} \max\quad & x\_var \\ -\text{Subject to} \\ +\text{Subject to}\\ & \text{SingleVariable-in-ZeroOne} \\ & x\_var \in \{0, 1\} \\ \end{aligned} $$ ``` !!! tip - In IJulia, calling `print` or ending a cell with [`latex_formulation`](@ref) - will render the model in LaTex. + In IJulia, calling `print` or ending a cell with + [`Utilities.latex_formulation`](@ref) will render the model in LaTex. ## Copy utilities diff --git a/src/Utilities/print.jl b/src/Utilities/print.jl index dfd7446c66..d2debd2535 100644 --- a/src/Utilities/print.jl +++ b/src/Utilities/print.jl @@ -50,7 +50,11 @@ function _to_string(::MIME, model::MOI.ModelLike, v::MOI.VariableIndex) return isempty(var_name) ? "noname" : var_name end -function _to_string(::MIME"text/latex", model::MOI.ModelLike, v::MOI.VariableIndex) +function _to_string( + ::MIME"text/latex", + model::MOI.ModelLike, + v::MOI.VariableIndex, +) var_name = MOI.get(model, MOI.VariableName(), v) if isempty(var_name) return "noname" @@ -74,7 +78,11 @@ function _to_string(mime::MIME, model::MOI.ModelLike, f::MOI.SingleVariable) return _to_string(mime, model, f.variable) end -function _to_string(mime::MIME, model::MOI.ModelLike, term::MOI.ScalarAffineTerm) +function _to_string( + mime::MIME, + model::MOI.ModelLike, + term::MOI.ScalarAffineTerm, +) name = _to_string(mime, model, term.variable_index) if term.coefficient < 0 return string(" - ", string(-term.coefficient), " ", name) @@ -83,7 +91,11 @@ function _to_string(mime::MIME, model::MOI.ModelLike, term::MOI.ScalarAffineTerm end end -function _to_string(mime::MIME, model::MOI.ModelLike, f::MOI.ScalarAffineFunction) +function _to_string( + mime::MIME, + model::MOI.ModelLike, + f::MOI.ScalarAffineFunction, +) s = string(f.constant) for term in f.terms s *= _to_string(mime, model, term) @@ -91,7 +103,11 @@ function _to_string(mime::MIME, model::MOI.ModelLike, f::MOI.ScalarAffineFunctio return s end -function _to_string(mime::MIME, model::MOI.ModelLike, term::MOI.ScalarQuadraticTerm) +function _to_string( + mime::MIME, + model::MOI.ModelLike, + term::MOI.ScalarQuadraticTerm, +) name_1 = _to_string(mime, model, term.variable_index_1) name_2 = _to_string(mime, model, term.variable_index_2) name = if term.variable_index_1 == term.variable_index_2 @@ -106,7 +122,11 @@ function _to_string(mime::MIME, model::MOI.ModelLike, term::MOI.ScalarQuadraticT end end -function _to_string(mime::MIME, model::MOI.ModelLike, f::MOI.ScalarQuadraticFunction) +function _to_string( + mime::MIME, + model::MOI.ModelLike, + f::MOI.ScalarQuadraticFunction, +) s = string(f.constant) for term in f.affine_terms s *= _to_string(mime, model, term) @@ -117,7 +137,11 @@ function _to_string(mime::MIME, model::MOI.ModelLike, f::MOI.ScalarQuadraticFunc return s end -function _to_string(mime::MIME, model::MOI.ModelLike, f::MOI.AbstractVectorFunction) +function _to_string( + mime::MIME, + model::MOI.ModelLike, + f::MOI.AbstractVectorFunction, +) rows = map(fi -> _to_string(mime, model, fi), eachscalar(f)) max_length = maximum(length.(rows)) s = join(map(r -> string("│", rpad(r, max_length), "│"), rows), '\n') @@ -201,29 +225,154 @@ function _to_string(mime::MIME, model::MOI.ModelLike, cref::MOI.ConstraintIndex) return string(_to_string(mime, model, f), " ", _to_string(mime, s)) end +#------------------------------------------------------------------------ +# Nonlinear constraints +#------------------------------------------------------------------------ + +""" + _VariableNode + +A type used to work-around the default printing of Julia expressions. If we +subsititued the variable names into the expression and the converted to a +string, each variable would be printed with enclosing `"`. + +To work-around this, create a new type and overload `show`. +""" +struct _VariableNode + x::String +end +Base.show(io::IO, x::_VariableNode) = print(io, x.x) + +_replace_names(::MIME, ::MOI.ModelLike, x, ::Any) = x +function _replace_names(mime::MIME, model::MOI.ModelLike, x::Expr, lookup) + if x.head == :ref + return get!(lookup, x.args[2]) do + return _VariableNode(_to_string(mime, model, x.args[2])) + end + else + for (i, arg) in enumerate(x.args) + x.args[i] = _replace_names(mime, model, arg, lookup) + end + end + return x +end + +function _replace_nonlinear_latex(::MIME"text/latex", s::String) + s = replace(s, " * " => " \\times ") + s = replace(s, " >= " => " \\ge ") + s = replace(s, " <= " => " \\le ") + s = replace(s, " == " => " = ") + return s +end +_replace_nonlinear_latex(::MIME, s::String) = s + +function _print_nonlinear_constraints( + io::IO, + mime::MIME"text/plain", + model::MOI.ModelLike, + block::MOI.NLPBlockData, +) + lookup = Dict{MOI.VariableIndex,_VariableNode}() + println(io, "\nNonlinear") + has_expr = :ExprGraph in MOI.features_available(block.evaluator) + for (i, bound) in enumerate(block.constraint_bounds) + if has_expr + ex = MOI.constraint_expr(block.evaluator, i) + println(io, " ", _replace_names(mime, model, ex, lookup)) + else + println(io, " ", bound.lower, " <= g_$(i)(x) <= ", bound.upper) + end + end +end + +function _print_nonlinear_constraints( + io::IO, + mime::MIME"text/latex", + model::MOI.ModelLike, + block::MOI.NLPBlockData, +) + lookup = Dict{MOI.VariableIndex,_VariableNode}() + println(io, " & \\text{Nonlinear} \\\\") + has_expr = :ExprGraph in MOI.features_available(block.evaluator) + for (i, bound) in enumerate(block.constraint_bounds) + if has_expr + ex = MOI.constraint_expr(block.evaluator, i) + nl_c = string(_replace_names(mime, model, ex, lookup)) + println(io, " & ", _replace_nonlinear_latex(mime, nl_c), " \\\\") + else + println( + io, + "& ", + bound.lower, + " \\le g_$(i)(x) \\le ", + bound.upper, + " \\\\", + ) + end + end +end + +_print_nonlinear_constraints(::IO, ::MIME, ::MOI.ModelLike, ::Nothing) = nothing + +#------------------------------------------------------------------------ +# ObjectiveFunction +#------------------------------------------------------------------------ + +function _objective_function_string(mime::MIME, model::MOI.ModelLike, ::Nothing) + F = MOI.get(model, MOI.ObjectiveFunctionType()) + f = MOI.get(model, MOI.ObjectiveFunction{F}()) + return _drop_moi(F), _to_string(mime, model, f) +end + +function _objective_function_string( + mime::MIME, + model::MOI.ModelLike, + block::MOI.NLPBlockData, +) + lookup = Dict{MOI.VariableIndex,_VariableNode}() + if block.has_objective + f = "f(x)" + if :ExprGraph in MOI.features_available(block.evaluator) + ex = MOI.objective_expr(block.evaluator) + f = string(_replace_names(mime, model, ex, lookup)) + f = _replace_nonlinear_latex(mime, f) + end + return "Nonlinear", f + else + return _objective_function_string(mime, model, nothing) + end +end + #------------------------------------------------------------------------ # MOI.ModelLike #------------------------------------------------------------------------ +function _nlp_block(model::MOI.ModelLike) + try + block = MOI.get(model, MOI.NLPBlock()) + if :ExprGraph in MOI.features_available(block.evaluator) + MOI.initialize(block.evaluator, [:ExprGraph]) + end + return block + catch + return nothing + end +end + """ _print_model(io::IO, mime::MIME"text/plain", model::MOI.ModelLike) Print a plain-text formulation of `model` to `io`. """ function _print_model(io::IO, mime::MIME"text/plain", model::MOI.ModelLike) + nlp_block = _nlp_block(model) sense = MOI.get(model, MOI.ObjectiveSense()) - if sense == MOI.MAX_SENSE - F = MOI.get(model, MOI.ObjectiveFunctionType()) - println(io, "Maximize $(_drop_moi(F)):") - f = MOI.get(model, MOI.ObjectiveFunction{F}()) - println(io, " ", _to_string(mime, model, f)) - elseif sense == MOI.MIN_SENSE - F = MOI.get(model, MOI.ObjectiveFunctionType()) - println(io, "Minimize $(_drop_moi(F)):") - f = MOI.get(model, MOI.ObjectiveFunction{F}()) - println(io, " ", _to_string(mime, model, f)) - else + if sense == MOI.FEASIBILITY_SENSE println(io, "Feasibility") + else + F, f = _objective_function_string(mime, model, nlp_block) + sense_s = sense == MOI.MIN_SENSE ? "Minimize" : "Maximize" + println(io, sense_s, " ", F, ":\n ", f) end println(io, "\nSubject to:") for (F, S) in MOI.get(model, MOI.ListOfConstraints()) @@ -233,6 +382,7 @@ function _print_model(io::IO, mime::MIME"text/plain", model::MOI.ModelLike) println(io, " ", replace(s, '\n' => "\n ")) end end + _print_nonlinear_constraints(io, mime, model, nlp_block) return end @@ -242,20 +392,15 @@ end Print a LaTeX formulation of `model` to `io`. """ function _print_model(io::IO, mime::MIME"text/latex", model::MOI.ModelLike) + nlp_block = _nlp_block(model) println(io, "\$\$ \\begin{aligned}") sense = MOI.get(model, MOI.ObjectiveSense()) - if sense == MOI.MAX_SENSE - print(io, "\\max\\quad & ") - F = MOI.get(model, MOI.ObjectiveFunctionType()) - f = MOI.get(model, MOI.ObjectiveFunction{F}()) - println(io, _to_string(mime, model, f), " \\\\") - elseif sense == MOI.MIN_SENSE - print(io, "\\min\\quad & ") - F = MOI.get(model, MOI.ObjectiveFunctionType()) - f = MOI.get(model, MOI.ObjectiveFunction{F}()) - println(io, _to_string(mime, model, f), " \\\\") - else + if sense == MOI.FEASIBILITY_SENSE println(io, "\\text{feasibility}\\\\") + else + F, f = _objective_function_string(mime, model, nlp_block) + sense_s = sense == MOI.MIN_SENSE ? "min" : "max" + println(io, "\\", sense_s, "\\quad & ", f, " \\\\") end println(io, "\\text{Subject to}\\\\") for (F, S) in MOI.get(model, MOI.ListOfConstraints()) @@ -264,6 +409,7 @@ function _print_model(io::IO, mime::MIME"text/latex", model::MOI.ModelLike) println(io, " & ", _to_string(mime, model, cref), " \\\\") end end + _print_nonlinear_constraints(io, mime, model, nlp_block) return print(io, "\\end{aligned} \$\$") end diff --git a/test/Utilities/print.jl b/test/Utilities/print.jl index a737b8c748..e15b8a2727 100644 --- a/test/Utilities/print.jl +++ b/test/Utilities/print.jl @@ -283,8 +283,9 @@ function test_latex() c5: x + -1 * y == 0.0 """, ) - evaluated = - sprint(io -> show(io, MIME("text/latex"), MOIU.latex_formulation(model))) + evaluated = sprint( + io -> show(io, MIME("text/latex"), MOIU.latex_formulation(model)), + ) @test evaluated == raw""" $$ \begin{aligned} \min\quad & 2.0 + 1.0 x + 3.1 y - 1.2 z \\ @@ -325,6 +326,63 @@ function test_latex() \end{aligned} $$""" end +function test_nlp() + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + v = MOI.add_variables(model, 4) + l = [1.1, 1.2, 1.3, 1.4] + u = [5.1, 5.2, 5.3, 5.4] + MOI.add_constraint.(model, MOI.SingleVariable.(v), MOI.GreaterThan.(l)) + MOI.add_constraint.(model, MOI.SingleVariable.(v), MOI.LessThan.(u)) + for i in 1:4 + MOI.set(model, MOI.VariableName(), v[i], "x[$i]") + end + lb, ub = [25.0, 40.0], [Inf, 40.0] + evaluator = MOI.Test.HS071(true) + block_data = MOI.NLPBlockData(MOI.NLPBoundsPair.(lb, ub), evaluator, true) + MOI.set(model, MOI.NLPBlock(), block_data) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + @test sprint(print, model) == """ + Minimize Nonlinear: + x[1] * x[4] * (x[1] + x[2] + x[3]) + x[3] + + Subject to: + + SingleVariable-in-GreaterThan{Float64} + x[1] >= 1.1 + x[2] >= 1.2 + x[3] >= 1.3 + x[4] >= 1.4 + + SingleVariable-in-LessThan{Float64} + x[1] <= 5.1 + x[2] <= 5.2 + x[3] <= 5.3 + x[4] <= 5.4 + + Nonlinear + x[1] * x[2] * x[3] * x[4] >= 25.0 + x[1] ^ 2 + x[2] ^ 2 + x[3] ^ 2 + x[4] ^ 2 == 40.0 + """ + @test sprint(print, MOIU.latex_formulation(model)) == raw""" + $$ \begin{aligned} + \min\quad & x_{1} \times x_{4} \times (x_{1} + x_{2} + x_{3}) + x_{3} \\ + \text{Subject to}\\ + & \text{SingleVariable-in-GreaterThan{Float64}} \\ + & x_{1} \ge 1.1 \\ + & x_{2} \ge 1.2 \\ + & x_{3} \ge 1.3 \\ + & x_{4} \ge 1.4 \\ + & \text{SingleVariable-in-LessThan{Float64}} \\ + & x_{1} \le 5.1 \\ + & x_{2} \le 5.2 \\ + & x_{3} \le 5.3 \\ + & x_{4} \le 5.4 \\ + & \text{Nonlinear} \\ + & x_{1} \times x_{2} \times x_{3} \times x_{4} \ge 25.0 \\ + & x_{1} ^ 2 + x_{2} ^ 2 + x_{3} ^ 2 + x_{4} ^ 2 = 40.0 \\ + \end{aligned} $$""" +end + function runtests() for name in names(@__MODULE__; all = true) if startswith("$(name)", "test_") From cd3be5ee0f5c16cb7a0d8be95670148a2c082f02 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 19 Apr 2021 17:45:44 +1200 Subject: [PATCH 04/11] Fix on Windows --- test/Utilities/print.jl | 170 +++++++++++++++++++++++----------------- 1 file changed, 97 insertions(+), 73 deletions(-) diff --git a/test/Utilities/print.jl b/test/Utilities/print.jl index e15b8a2727..4a625e1538 100644 --- a/test/Utilities/print.jl +++ b/test/Utilities/print.jl @@ -10,6 +10,12 @@ const LATEX = MIME("text/latex") const PLAIN = MIME("text/plain") const IN = Sys.iswindows() ? "in" : "∈" +# Windows fun... +function _string_compare(a, b) + @test replace(a, "\r\n" => "\n") == replace(b, "\r\n" => "\n") + return +end + function test_nonname_variable() model = MOIU.Model{Float64}() x = MOI.add_variable(model) @@ -147,11 +153,15 @@ end function test_feasibility() model = MOIU.Model{Float64}() @test sprint(print, model) == "Feasibility\n\nSubject to:\n" - @test sprint(print, MOIU.latex_formulation(model)) == raw""" - $$ \begin{aligned} - \text{feasibility}\\ - \text{Subject to}\\ - \end{aligned} $$""" + _string_compare( + sprint(print, MOIU.latex_formulation(model)), + raw""" + $$ \begin{aligned} + \text{feasibility}\\ + \text{Subject to}\\ + \end{aligned} $$""", + ) + return end function test_min() @@ -163,11 +173,15 @@ function test_min() Subject to: """ - @test sprint(print, MOIU.latex_formulation(model)) == raw""" - $$ \begin{aligned} - \min\quad & x \\ - \text{Subject to}\\ - \end{aligned} $$""" + _string_compare( + sprint(print, MOIU.latex_formulation(model)), + raw""" + $$ \begin{aligned} + \min\quad & x \\ + \text{Subject to}\\ + \end{aligned} $$""", + ) + return end function test_max() @@ -179,11 +193,15 @@ function test_max() Subject to: """ - @test sprint(print, MOIU.latex_formulation(model)) == raw""" - $$ \begin{aligned} - \max\quad & x \\ - \text{Subject to}\\ - \end{aligned} $$""" + _string_compare( + sprint(print, MOIU.latex_formulation(model)), + raw""" + $$ \begin{aligned} + \max\quad & x \\ + \text{Subject to}\\ + \end{aligned} $$""", + ) + return end function test_model() @@ -283,47 +301,50 @@ function test_latex() c5: x + -1 * y == 0.0 """, ) - evaluated = sprint( - io -> show(io, MIME("text/latex"), MOIU.latex_formulation(model)), + _string_compare( + sprint( + io -> show(io, MIME("text/latex"), MOIU.latex_formulation(model)), + ), + raw""" + $$ \begin{aligned} + \min\quad & 2.0 + 1.0 x + 3.1 y - 1.2 z \\ + \text{Subject to}\\ + & \text{ScalarAffineFunction{Float64}-in-EqualTo{Float64}} \\ + & 0.0 + 1.0 x - 1.0 y = 0.0 \\ + & \text{ScalarAffineFunction{Float64}-in-GreaterThan{Float64}} \\ + & 0.0 + 2.0 x \ge 1.0 \\ + & \text{ScalarAffineFunction{Float64}-in-Interval{Float64}} \\ + & 0.0 + 2.0 x \in \[1.0, 2.0\] \\ + & \text{ScalarQuadraticFunction{Float64}-in-LessThan{Float64}} \\ + & 0.0 + 1.0 y - 1.0 z + 4.0 x^2 \le 1.0 \\ + & \text{VectorOfVariables-in-SecondOrderCone} \\ + & \begin{bmatrix} + x\\ + y\end{bmatrix} \in \text{SecondOrderCone(2)} \\ + & \text{VectorAffineFunction{Float64}-in-SecondOrderCone} \\ + & \begin{bmatrix} + 1.0\\ + 0.0 + 1.0 x\\ + 0.0 + 1.0 y\end{bmatrix} \in \text{SecondOrderCone(2)} \\ + & \text{VectorQuadraticFunction{Float64}-in-ExponentialCone} \\ + & \begin{bmatrix} + 0.0 + 2.0 x^2\\ + 0.0 + 1.0 y\\ + 1.0\end{bmatrix} \in \text{ExponentialCone()} \\ + & \begin{bmatrix} + 1.0\\ + 0.0 + 2.0 x^2\\ + 0.0 + 1.0 y\end{bmatrix} \in \text{ExponentialCone()} \\ + & \text{SingleVariable-in-GreaterThan{Float64}} \\ + & x \ge 0.1 \\ + & \text{SingleVariable-in-Integer} \\ + & z \in \mathbb{Z} \\ + & \text{SingleVariable-in-ZeroOne} \\ + & x \in \{0, 1\} \\ + & y \in \{0, 1\} \\ + \end{aligned} $$""", ) - @test evaluated == raw""" - $$ \begin{aligned} - \min\quad & 2.0 + 1.0 x + 3.1 y - 1.2 z \\ - \text{Subject to}\\ - & \text{ScalarAffineFunction{Float64}-in-EqualTo{Float64}} \\ - & 0.0 + 1.0 x - 1.0 y = 0.0 \\ - & \text{ScalarAffineFunction{Float64}-in-GreaterThan{Float64}} \\ - & 0.0 + 2.0 x \ge 1.0 \\ - & \text{ScalarAffineFunction{Float64}-in-Interval{Float64}} \\ - & 0.0 + 2.0 x \in \[1.0, 2.0\] \\ - & \text{ScalarQuadraticFunction{Float64}-in-LessThan{Float64}} \\ - & 0.0 + 1.0 y - 1.0 z + 4.0 x^2 \le 1.0 \\ - & \text{VectorOfVariables-in-SecondOrderCone} \\ - & \begin{bmatrix} - x\\ - y\end{bmatrix} \in \text{SecondOrderCone(2)} \\ - & \text{VectorAffineFunction{Float64}-in-SecondOrderCone} \\ - & \begin{bmatrix} - 1.0\\ - 0.0 + 1.0 x\\ - 0.0 + 1.0 y\end{bmatrix} \in \text{SecondOrderCone(2)} \\ - & \text{VectorQuadraticFunction{Float64}-in-ExponentialCone} \\ - & \begin{bmatrix} - 0.0 + 2.0 x^2\\ - 0.0 + 1.0 y\\ - 1.0\end{bmatrix} \in \text{ExponentialCone()} \\ - & \begin{bmatrix} - 1.0\\ - 0.0 + 2.0 x^2\\ - 0.0 + 1.0 y\end{bmatrix} \in \text{ExponentialCone()} \\ - & \text{SingleVariable-in-GreaterThan{Float64}} \\ - & x \ge 0.1 \\ - & \text{SingleVariable-in-Integer} \\ - & z \in \mathbb{Z} \\ - & \text{SingleVariable-in-ZeroOne} \\ - & x \in \{0, 1\} \\ - & y \in \{0, 1\} \\ - \end{aligned} $$""" + return end function test_nlp() @@ -363,24 +384,27 @@ function test_nlp() x[1] * x[2] * x[3] * x[4] >= 25.0 x[1] ^ 2 + x[2] ^ 2 + x[3] ^ 2 + x[4] ^ 2 == 40.0 """ - @test sprint(print, MOIU.latex_formulation(model)) == raw""" - $$ \begin{aligned} - \min\quad & x_{1} \times x_{4} \times (x_{1} + x_{2} + x_{3}) + x_{3} \\ - \text{Subject to}\\ - & \text{SingleVariable-in-GreaterThan{Float64}} \\ - & x_{1} \ge 1.1 \\ - & x_{2} \ge 1.2 \\ - & x_{3} \ge 1.3 \\ - & x_{4} \ge 1.4 \\ - & \text{SingleVariable-in-LessThan{Float64}} \\ - & x_{1} \le 5.1 \\ - & x_{2} \le 5.2 \\ - & x_{3} \le 5.3 \\ - & x_{4} \le 5.4 \\ - & \text{Nonlinear} \\ - & x_{1} \times x_{2} \times x_{3} \times x_{4} \ge 25.0 \\ - & x_{1} ^ 2 + x_{2} ^ 2 + x_{3} ^ 2 + x_{4} ^ 2 = 40.0 \\ - \end{aligned} $$""" + _string_compare( + sprint(print, MOIU.latex_formulation(model)), + raw""" + $$ \begin{aligned} + \min\quad & x_{1} \times x_{4} \times (x_{1} + x_{2} + x_{3}) + x_{3} \\ + \text{Subject to}\\ + & \text{SingleVariable-in-GreaterThan{Float64}} \\ + & x_{1} \ge 1.1 \\ + & x_{2} \ge 1.2 \\ + & x_{3} \ge 1.3 \\ + & x_{4} \ge 1.4 \\ + & \text{SingleVariable-in-LessThan{Float64}} \\ + & x_{1} \le 5.1 \\ + & x_{2} \le 5.2 \\ + & x_{3} \le 5.3 \\ + & x_{4} \le 5.4 \\ + & \text{Nonlinear} \\ + & x_{1} \times x_{2} \times x_{3} \times x_{4} \ge 25.0 \\ + & x_{1} ^ 2 + x_{2} ^ 2 + x_{3} ^ 2 + x_{4} ^ 2 = 40.0 \\ + \end{aligned} $$""", + ) end function runtests() From 0bb890146783ec61712bcc7f281a1fa152624095 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 19 Apr 2021 19:29:55 +1200 Subject: [PATCH 05/11] Update print.jl --- test/Utilities/print.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Utilities/print.jl b/test/Utilities/print.jl index 4a625e1538..a134247bde 100644 --- a/test/Utilities/print.jl +++ b/test/Utilities/print.jl @@ -303,7 +303,7 @@ function test_latex() ) _string_compare( sprint( - io -> show(io, MIME("text/latex"), MOIU.latex_formulation(model)), + io -> show(io, MIME("text/latex"), MOIU.latex_formulation(model)), ), raw""" $$ \begin{aligned} @@ -405,6 +405,7 @@ function test_nlp() & x_{1} ^ 2 + x_{2} ^ 2 + x_{3} ^ 2 + x_{4} ^ 2 = 40.0 \\ \end{aligned} $$""", ) + return end function runtests() From c28f73c1d9ab139e48944a6fe5af6a75f9d374dc Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 20 Apr 2021 10:46:22 +1200 Subject: [PATCH 06/11] Fix quadratic printing --- src/Utilities/print.jl | 10 +++++++--- test/Utilities/print.jl | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Utilities/print.jl b/src/Utilities/print.jl index d2debd2535..9762e0766e 100644 --- a/src/Utilities/print.jl +++ b/src/Utilities/print.jl @@ -110,15 +110,19 @@ function _to_string( ) name_1 = _to_string(mime, model, term.variable_index_1) name_2 = _to_string(mime, model, term.variable_index_2) + # Be careful here when printing the coefficient. ScalarQuadraticFunction + # assumes an additional 0.5 factor! + coef = term.coefficient name = if term.variable_index_1 == term.variable_index_2 + coef /= 2 string(" ", name_1, _to_string(mime, :sq)) else string(" ", name_1, _to_string(mime, :times), name_2) end - if term.coefficient < 0 - return string(" - ", string(-term.coefficient), name) + if coef < 0 + return string(" - ", string(-coef), name) else - return string(" + ", string(term.coefficient), name) + return string(" + ", string(coef), name) end end diff --git a/test/Utilities/print.jl b/test/Utilities/print.jl index 4a625e1538..f20ac7edce 100644 --- a/test/Utilities/print.jl +++ b/test/Utilities/print.jl @@ -72,8 +72,8 @@ function test_ScalarQuadraticTerm() MOI.set(model, MOI.VariableName(), x, "x") MOI.set(model, MOI.VariableName(), y, "y") term = MOI.ScalarQuadraticTerm(-1.2, x, x) - @test MOIU._to_string(PLAIN, model, term) == " - 1.2 x²" - @test MOIU._to_string(LATEX, model, term) == " - 1.2 x^2" + @test MOIU._to_string(PLAIN, model, term) == " - 0.6 x²" + @test MOIU._to_string(LATEX, model, term) == " - 0.6 x^2" term = MOI.ScalarQuadraticTerm(1.2, x, y) @test MOIU._to_string(PLAIN, model, term) == " + 1.2 x*y" @test MOIU._to_string(LATEX, model, term) == " + 1.2 x\\times y" @@ -91,9 +91,9 @@ function test_ScalarQuadraticFunction() 1.4, ) @test MOIU._to_string(PLAIN, model, f) == - "1.4 - 1.2 x + 1.3 x + 0.5 x² + 0.6 x*y" + "1.4 - 1.2 x + 1.3 x + 0.25 x² + 0.6 x*y" @test MOIU._to_string(LATEX, model, f) == - "1.4 - 1.2 x + 1.3 x + 0.5 x^2 + 0.6 x\\times y" + "1.4 - 1.2 x + 1.3 x + 0.25 x^2 + 0.6 x\\times y" end function test_VectorOfVariables() @@ -241,7 +241,7 @@ function test_model() 0.0 + 2.0 x $(IN) [1.0, 2.0] ScalarQuadraticFunction{Float64}-in-LessThan{Float64} - 0.0 + 1.0 y - 1.0 z + 4.0 x² <= 1.0 + 0.0 + 1.0 y - 1.0 z + 2.0 x² <= 1.0 VectorOfVariables-in-SecondOrderCone ┌ ┐ @@ -258,13 +258,13 @@ function test_model() VectorQuadraticFunction{Float64}-in-ExponentialCone ┌ ┐ - │0.0 + 2.0 x²│ + │0.0 + 1.0 x²│ │0.0 + 1.0 y │ │1.0 │ └ ┘ $(IN) ExponentialCone() ┌ ┐ │1.0 │ - │0.0 + 2.0 x²│ + │0.0 + 1.0 x²│ │0.0 + 1.0 y │ └ ┘ $(IN) ExponentialCone() @@ -316,7 +316,7 @@ function test_latex() & \text{ScalarAffineFunction{Float64}-in-Interval{Float64}} \\ & 0.0 + 2.0 x \in \[1.0, 2.0\] \\ & \text{ScalarQuadraticFunction{Float64}-in-LessThan{Float64}} \\ - & 0.0 + 1.0 y - 1.0 z + 4.0 x^2 \le 1.0 \\ + & 0.0 + 1.0 y - 1.0 z + 2.0 x^2 \le 1.0 \\ & \text{VectorOfVariables-in-SecondOrderCone} \\ & \begin{bmatrix} x\\ @@ -328,12 +328,12 @@ function test_latex() 0.0 + 1.0 y\end{bmatrix} \in \text{SecondOrderCone(2)} \\ & \text{VectorQuadraticFunction{Float64}-in-ExponentialCone} \\ & \begin{bmatrix} - 0.0 + 2.0 x^2\\ + 0.0 + 1.0 x^2\\ 0.0 + 1.0 y\\ 1.0\end{bmatrix} \in \text{ExponentialCone()} \\ & \begin{bmatrix} 1.0\\ - 0.0 + 2.0 x^2\\ + 0.0 + 1.0 x^2\\ 0.0 + 1.0 y\end{bmatrix} \in \text{ExponentialCone()} \\ & \text{SingleVariable-in-GreaterThan{Float64}} \\ & x \ge 0.1 \\ From eb831a317e295d1a83b784421112b415cf3aa07d Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 21 Apr 2021 07:41:53 +1200 Subject: [PATCH 07/11] [ci skip] update docstring --- src/Utilities/print.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Utilities/print.jl b/src/Utilities/print.jl index 9762e0766e..c86a6b7e23 100644 --- a/src/Utilities/print.jl +++ b/src/Utilities/print.jl @@ -236,9 +236,11 @@ end """ _VariableNode -A type used to work-around the default printing of Julia expressions. If we -subsititued the variable names into the expression and the converted to a -string, each variable would be printed with enclosing `"`. +A type used to work-around the default printing of Julia expressions. + +Without this type, if we subsititued the variable names into the expression +and then converted to a string, each variable would be printed with enclosing +`"`. To work-around this, create a new type and overload `show`. """ From 7869b2a09735a2b0b84f297d87841028a1ba73fa Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 30 Apr 2021 11:47:18 +1200 Subject: [PATCH 08/11] Add _PrintOptions --- src/Utilities/print.jl | 356 +++++++++++++++++++++++++++++----------- test/Utilities/print.jl | 185 +++++++++++++++++++-- 2 files changed, 433 insertions(+), 108 deletions(-) diff --git a/src/Utilities/print.jl b/src/Utilities/print.jl index c86a6b7e23..f23ed8d132 100644 --- a/src/Utilities/print.jl +++ b/src/Utilities/print.jl @@ -2,6 +2,29 @@ using Printf _drop_moi(s) = replace(string(s), "MathOptInterface." => "") +struct _PrintOptions{T<:MIME} + simplify_coefficients::Bool + default_name::String + print_constraint_types::Bool + + function _PrintOptions( + mime::MIME; + simplify_coefficients::Bool = false, + default_name::String = "v", + print_constraint_types::Bool = true, + ) + return new{typeof(mime)}( + simplify_coefficients, + default_name, + print_constraint_types, + ) + end +end + +function _to_string(mime::MIME, args...; kwargs...) + return _to_string(_PrintOptions(mime), args...; kwargs...) +end + #------------------------------------------------------------------------ # Math Symbols #------------------------------------------------------------------------ @@ -9,7 +32,7 @@ _drop_moi(s) = replace(string(s), "MathOptInterface." => "") # REPL-specific symbols # Anything here: https://en.wikipedia.org/wiki/Windows-1252 # should probably work fine on Windows -function _to_string(::MIME, name::Symbol) +function _to_string(::_PrintOptions, name::Symbol) if name == :leq return "<=" elseif name == :geq @@ -25,7 +48,7 @@ function _to_string(::MIME, name::Symbol) end end -function _to_string(::MIME"text/latex", name::Symbol) +function _to_string(::_PrintOptions{MIME"text/latex"}, name::Symbol) if name == :leq return "\\leq" elseif name == :geq @@ -45,19 +68,27 @@ end # Functions #------------------------------------------------------------------------ -function _to_string(::MIME, model::MOI.ModelLike, v::MOI.VariableIndex) +function _to_string( + options::_PrintOptions, + model::MOI.ModelLike, + v::MOI.VariableIndex, +) var_name = MOI.get(model, MOI.VariableName(), v) - return isempty(var_name) ? "noname" : var_name + if isempty(var_name) + return string(options.default_name, "[", v.value, "]") + else + return var_name + end end function _to_string( - ::MIME"text/latex", + options::_PrintOptions{MIME"text/latex"}, model::MOI.ModelLike, v::MOI.VariableIndex, ) var_name = MOI.get(model, MOI.VariableName(), v) if isempty(var_name) - return "noname" + return string(options.default_name, "_{", v.value, "}") end # We need to escape latex math characters that appear in the name. # However, it's probably impractical to catch everything, so let's just @@ -74,79 +105,129 @@ function _to_string( return var_name end -function _to_string(mime::MIME, model::MOI.ModelLike, f::MOI.SingleVariable) - return _to_string(mime, model, f.variable) +function _shorten(options::_PrintOptions, x::Float64) + if options.simplify_coefficients && isinteger(x) + return string(round(Int, x)) + end + return string(x) end function _to_string( - mime::MIME, + options::_PrintOptions, model::MOI.ModelLike, - term::MOI.ScalarAffineTerm, + f::MOI.SingleVariable, ) - name = _to_string(mime, model, term.variable_index) - if term.coefficient < 0 - return string(" - ", string(-term.coefficient), " ", name) + return _to_string(options, model, f.variable) +end + +""" + _to_string(options::_PrintOptions, c::Float64, x::String) + +Write a coefficient-name pair to string. There are a few cases to handle. + + | is_first | !is_first + ----------------------------- + +2.1x | "2.1 x" | " + 2.1 x" + -2.1x | "-2.1 x" | " - 2.1 x" + ----------------------------- + +2.0x | "2 x" | " + 2 x" + -2.0x | "-2 x" | " - 2 x" + +1.0x | "x" | " + x" + -1.0x | "-x" | " - x" +""" +function _to_string( + options::_PrintOptions, + c::Float64, + x::String; + is_first::Bool, +) + prefix = if is_first + c < 0 ? "-" : "" else - return string(" + ", string(term.coefficient), " ", name) + c < 0 ? " - " : " + " end + s = _shorten(options, abs(c)) + if options.simplify_coefficients && s == "1" + return string(prefix, x) + else + return string(prefix, s, " ", x) + end +end + +function _to_string( + options::_PrintOptions, + model::MOI.ModelLike, + term::MOI.ScalarAffineTerm; + is_first::Bool, +) + name = _to_string(options, model, term.variable_index) + return _to_string(options, term.coefficient, name; is_first = is_first) end function _to_string( - mime::MIME, + options::_PrintOptions, model::MOI.ModelLike, f::MOI.ScalarAffineFunction, ) - s = string(f.constant) + s = _shorten(options, f.constant) + if options.simplify_coefficients && iszero(f.constant) + s = "" + end + is_first = isempty(s) for term in f.terms - s *= _to_string(mime, model, term) + s *= _to_string(options, model, term; is_first = is_first) + is_first = false end return s end function _to_string( - mime::MIME, + options::_PrintOptions, model::MOI.ModelLike, - term::MOI.ScalarQuadraticTerm, + term::MOI.ScalarQuadraticTerm; + is_first::Bool, ) - name_1 = _to_string(mime, model, term.variable_index_1) - name_2 = _to_string(mime, model, term.variable_index_2) + name_1 = _to_string(options, model, term.variable_index_1) + name_2 = _to_string(options, model, term.variable_index_2) # Be careful here when printing the coefficient. ScalarQuadraticFunction # assumes an additional 0.5 factor! coef = term.coefficient name = if term.variable_index_1 == term.variable_index_2 coef /= 2 - string(" ", name_1, _to_string(mime, :sq)) - else - string(" ", name_1, _to_string(mime, :times), name_2) - end - if coef < 0 - return string(" - ", string(-coef), name) + string(name_1, _to_string(options, :sq)) else - return string(" + ", string(coef), name) + string(name_1, _to_string(options, :times), name_2) end + return _to_string(options, coef, name; is_first = is_first) end function _to_string( - mime::MIME, + options::_PrintOptions, model::MOI.ModelLike, f::MOI.ScalarQuadraticFunction, ) - s = string(f.constant) + s = _shorten(options, f.constant) + if options.simplify_coefficients && iszero(f.constant) + s = "" + end + is_first = isempty(s) for term in f.affine_terms - s *= _to_string(mime, model, term) + s *= _to_string(options, model, term; is_first = is_first) + is_first = false end for term in f.quadratic_terms - s *= _to_string(mime, model, term) + s *= _to_string(options, model, term; is_first = is_first) + is_first = false end return s end function _to_string( - mime::MIME, + options::_PrintOptions, model::MOI.ModelLike, f::MOI.AbstractVectorFunction, ) - rows = map(fi -> _to_string(mime, model, fi), eachscalar(f)) + rows = map(fi -> _to_string(options, model, fi), eachscalar(f)) max_length = maximum(length.(rows)) s = join(map(r -> string("│", rpad(r, max_length), "│"), rows), '\n') return string( @@ -161,13 +242,16 @@ function _to_string( end function _to_string( - mime::MIME"text/latex", + options::_PrintOptions{MIME"text/latex"}, model::MOI.ModelLike, f::MOI.AbstractVectorFunction, ) return string( "\\begin{bmatrix}\n", - join(map(fi -> _to_string(mime, model, fi), eachscalar(f)), "\\\\\n"), + join( + map(fi -> _to_string(options, model, fi), eachscalar(f)), + "\\\\\n", + ), "\\end{bmatrix}", ) end @@ -176,45 +260,73 @@ end # Sets #------------------------------------------------------------------------ -function _to_string(mime::MIME, set::MOI.LessThan) - return string(_to_string(mime, :leq), " ", set.upper) +function _to_string(options::_PrintOptions, set::MOI.LessThan) + return string(_to_string(options, :leq), " ", _shorten(options, set.upper)) end -_to_string(::MIME"text/latex", set::MOI.LessThan) = "\\le $(set.upper)" +function _to_string(options::_PrintOptions{MIME"text/latex"}, set::MOI.LessThan) + return string("\\le ", _shorten(options, set.upper)) +end -function _to_string(mime::MIME, set::MOI.GreaterThan) - return string(_to_string(mime, :geq), " ", set.lower) +function _to_string(options::_PrintOptions, set::MOI.GreaterThan) + return string(_to_string(options, :geq), " ", _shorten(options, set.lower)) end -_to_string(::MIME"text/latex", set::MOI.GreaterThan) = "\\ge $(set.lower)" +function _to_string( + options::_PrintOptions{MIME"text/latex"}, + set::MOI.GreaterThan, +) + return string("\\ge ", _shorten(options, set.lower)) +end -function _to_string(mime::MIME, set::MOI.EqualTo) - return string(_to_string(mime, :eq), " ", set.value) +function _to_string(options::_PrintOptions, set::MOI.EqualTo) + return string(_to_string(options, :eq), " ", _shorten(options, set.value)) end -_to_string(::MIME"text/latex", set::MOI.EqualTo) = "= $(set.value)" +function _to_string(options::_PrintOptions{MIME"text/latex"}, set::MOI.EqualTo) + return string("= ", _shorten(options, set.value)) +end -function _to_string(mime::MIME, set::MOI.Interval) - return string(_to_string(mime, :in), " [", set.lower, ", ", set.upper, "]") +function _to_string(options::_PrintOptions, set::MOI.Interval) + return string( + _to_string(options, :in), + " [", + _shorten(options, set.lower), + ", ", + _shorten(options, set.upper), + "]", + ) end -function _to_string(::MIME"text/latex", set::MOI.Interval) - return "\\in \\[$(set.lower), $(set.upper)\\]" +function _to_string(options::_PrintOptions{MIME"text/latex"}, set::MOI.Interval) + return string( + "\\in \\[", + _shorten(options, set.lower), + ", ", + _shorten(options, set.upper), + "\\]", + ) end -_to_string(mime::MIME, ::MOI.ZeroOne) = string(_to_string(mime, :in), " {0, 1}") +function _to_string(options::_PrintOptions, ::MOI.ZeroOne) + return string(_to_string(options, :in), " {0, 1}") +end -_to_string(::MIME"text/latex", ::MOI.ZeroOne) = "\\in \\{0, 1\\}" +_to_string(::_PrintOptions{MIME"text/latex"}, ::MOI.ZeroOne) = "\\in \\{0, 1\\}" -_to_string(mime::MIME, ::MOI.Integer) = string(_to_string(mime, :in), " ℤ") +function _to_string(options::_PrintOptions, ::MOI.Integer) + return string(_to_string(options, :in), " ℤ") +end -_to_string(::MIME"text/latex", ::MOI.Integer) = "\\in \\mathbb{Z}" +function _to_string(::_PrintOptions{MIME"text/latex"}, ::MOI.Integer) + return "\\in \\mathbb{Z}" +end -function _to_string(mime::MIME, set::MOI.AbstractSet) - return string(_to_string(mime, :in), " ", _drop_moi(set)) +function _to_string(options::_PrintOptions, set::MOI.AbstractSet) + return string(_to_string(options, :in), " ", _drop_moi(set)) end -function _to_string(::MIME"text/latex", set::MOI.AbstractSet) +function _to_string(::_PrintOptions{MIME"text/latex"}, set::MOI.AbstractSet) set_str = replace(replace(_drop_moi(set), "{" => "\\{"), "}" => "\\}") return string("\\in \\text{", set_str, "}") end @@ -223,10 +335,14 @@ end # Constraints #------------------------------------------------------------------------ -function _to_string(mime::MIME, model::MOI.ModelLike, cref::MOI.ConstraintIndex) +function _to_string( + options::_PrintOptions, + model::MOI.ModelLike, + cref::MOI.ConstraintIndex, +) f = MOI.get(model, MOI.ConstraintFunction(), cref) s = MOI.get(model, MOI.ConstraintSet(), cref) - return string(_to_string(mime, model, f), " ", _to_string(mime, s)) + return string(_to_string(options, model, f), " ", _to_string(options, s)) end #------------------------------------------------------------------------ @@ -236,10 +352,10 @@ end """ _VariableNode -A type used to work-around the default printing of Julia expressions. +A type used to work-around the default printing of Julia expressions. -Without this type, if we subsititued the variable names into the expression -and then converted to a string, each variable would be printed with enclosing +Without this type, if we subsititued the variable names into the expression +and then converted to a string, each variable would be printed with enclosing `"`. To work-around this, create a new type and overload `show`. @@ -249,42 +365,49 @@ struct _VariableNode end Base.show(io::IO, x::_VariableNode) = print(io, x.x) -_replace_names(::MIME, ::MOI.ModelLike, x, ::Any) = x -function _replace_names(mime::MIME, model::MOI.ModelLike, x::Expr, lookup) +_replace_names(::_PrintOptions, ::MOI.ModelLike, x, ::Any) = x +function _replace_names( + options::_PrintOptions, + model::MOI.ModelLike, + x::Expr, + lookup, +) if x.head == :ref return get!(lookup, x.args[2]) do - return _VariableNode(_to_string(mime, model, x.args[2])) + return _VariableNode(_to_string(options, model, x.args[2])) end else for (i, arg) in enumerate(x.args) - x.args[i] = _replace_names(mime, model, arg, lookup) + x.args[i] = _replace_names(options, model, arg, lookup) end end return x end -function _replace_nonlinear_latex(::MIME"text/latex", s::String) +function _replace_nonlinear_latex(::_PrintOptions{MIME"text/latex"}, s::String) s = replace(s, " * " => " \\times ") s = replace(s, " >= " => " \\ge ") s = replace(s, " <= " => " \\le ") s = replace(s, " == " => " = ") return s end -_replace_nonlinear_latex(::MIME, s::String) = s +_replace_nonlinear_latex(::_PrintOptions, s::String) = s function _print_nonlinear_constraints( io::IO, - mime::MIME"text/plain", + options::_PrintOptions{MIME"text/plain"}, model::MOI.ModelLike, block::MOI.NLPBlockData, ) lookup = Dict{MOI.VariableIndex,_VariableNode}() - println(io, "\nNonlinear") + if options.print_constraint_types + println(io, "\nNonlinear") + end has_expr = :ExprGraph in MOI.features_available(block.evaluator) for (i, bound) in enumerate(block.constraint_bounds) if has_expr ex = MOI.constraint_expr(block.evaluator, i) - println(io, " ", _replace_names(mime, model, ex, lookup)) + println(io, " ", _replace_names(options, model, ex, lookup)) else println(io, " ", bound.lower, " <= g_$(i)(x) <= ", bound.upper) end @@ -293,18 +416,20 @@ end function _print_nonlinear_constraints( io::IO, - mime::MIME"text/latex", + options::_PrintOptions{MIME"text/latex"}, model::MOI.ModelLike, block::MOI.NLPBlockData, ) lookup = Dict{MOI.VariableIndex,_VariableNode}() - println(io, " & \\text{Nonlinear} \\\\") + if options.print_constraint_types + println(io, " & \\text{Nonlinear} \\\\") + end has_expr = :ExprGraph in MOI.features_available(block.evaluator) for (i, bound) in enumerate(block.constraint_bounds) if has_expr ex = MOI.constraint_expr(block.evaluator, i) - nl_c = string(_replace_names(mime, model, ex, lookup)) - println(io, " & ", _replace_nonlinear_latex(mime, nl_c), " \\\\") + nl_c = string(_replace_names(options, model, ex, lookup)) + println(io, " & ", _replace_nonlinear_latex(options, nl_c), " \\\\") else println( io, @@ -318,20 +443,31 @@ function _print_nonlinear_constraints( end end -_print_nonlinear_constraints(::IO, ::MIME, ::MOI.ModelLike, ::Nothing) = nothing +function _print_nonlinear_constraints( + ::IO, + ::_PrintOptions, + ::MOI.ModelLike, + ::Nothing, +) + return +end #------------------------------------------------------------------------ # ObjectiveFunction #------------------------------------------------------------------------ -function _objective_function_string(mime::MIME, model::MOI.ModelLike, ::Nothing) +function _objective_function_string( + options::_PrintOptions, + model::MOI.ModelLike, + ::Nothing, +) F = MOI.get(model, MOI.ObjectiveFunctionType()) f = MOI.get(model, MOI.ObjectiveFunction{F}()) - return _drop_moi(F), _to_string(mime, model, f) + return _drop_moi(F), _to_string(options, model, f) end function _objective_function_string( - mime::MIME, + options::_PrintOptions, model::MOI.ModelLike, block::MOI.NLPBlockData, ) @@ -340,12 +476,12 @@ function _objective_function_string( f = "f(x)" if :ExprGraph in MOI.features_available(block.evaluator) ex = MOI.objective_expr(block.evaluator) - f = string(_replace_names(mime, model, ex, lookup)) - f = _replace_nonlinear_latex(mime, f) + f = string(_replace_names(options, model, ex, lookup)) + f = _replace_nonlinear_latex(options, f) end return "Nonlinear", f else - return _objective_function_string(mime, model, nothing) + return _objective_function_string(options, model, nothing) end end @@ -366,56 +502,80 @@ function _nlp_block(model::MOI.ModelLike) end """ - _print_model(io::IO, mime::MIME"text/plain", model::MOI.ModelLike) + _print_model( + io::IO, + options::_PrintOptions{MIME"text/plain"}, + model::MOI.ModelLike, + ) Print a plain-text formulation of `model` to `io`. """ -function _print_model(io::IO, mime::MIME"text/plain", model::MOI.ModelLike) +function _print_model( + io::IO, + options::_PrintOptions{MIME"text/plain"}, + model::MOI.ModelLike, +) nlp_block = _nlp_block(model) sense = MOI.get(model, MOI.ObjectiveSense()) if sense == MOI.FEASIBILITY_SENSE println(io, "Feasibility") else - F, f = _objective_function_string(mime, model, nlp_block) + F, f = _objective_function_string(options, model, nlp_block) sense_s = sense == MOI.MIN_SENSE ? "Minimize" : "Maximize" - println(io, sense_s, " ", F, ":\n ", f) + if options.print_constraint_types + println(io, sense_s, " ", F, ":\n ", f) + else + println(io, sense_s, ": ", f) + end end println(io, "\nSubject to:") for (F, S) in MOI.get(model, MOI.ListOfConstraints()) - println(io, "\n$(_drop_moi(F))-in-$(_drop_moi(S))") + if options.print_constraint_types + println(io, "\n$(_drop_moi(F))-in-$(_drop_moi(S))") + end for cref in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) - s = _to_string(mime, model, cref) + s = _to_string(options, model, cref) println(io, " ", replace(s, '\n' => "\n ")) end end - _print_nonlinear_constraints(io, mime, model, nlp_block) + _print_nonlinear_constraints(io, options, model, nlp_block) return end """ - _print_model(io::IO, mime::MIME"text/latex", model::MOI.ModelLike) + _print_model( + io::IO, + options::_PrintOptions{MIME"text/latex"}, + model::MOI.ModelLike, + ) Print a LaTeX formulation of `model` to `io`. """ -function _print_model(io::IO, mime::MIME"text/latex", model::MOI.ModelLike) +function _print_model( + io::IO, + options::_PrintOptions{MIME"text/latex"}, + model::MOI.ModelLike, +) nlp_block = _nlp_block(model) println(io, "\$\$ \\begin{aligned}") sense = MOI.get(model, MOI.ObjectiveSense()) if sense == MOI.FEASIBILITY_SENSE println(io, "\\text{feasibility}\\\\") else - F, f = _objective_function_string(mime, model, nlp_block) + F, f = _objective_function_string(options, model, nlp_block) sense_s = sense == MOI.MIN_SENSE ? "min" : "max" println(io, "\\", sense_s, "\\quad & ", f, " \\\\") end println(io, "\\text{Subject to}\\\\") for (F, S) in MOI.get(model, MOI.ListOfConstraints()) - println(io, " & \\text{$(_drop_moi(F))-in-$(_drop_moi(S))} \\\\") + if options.print_constraint_types + println(io, " & \\text{$(_drop_moi(F))-in-$(_drop_moi(S))} \\\\") + end for cref in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) - println(io, " & ", _to_string(mime, model, cref), " \\\\") + println(io, " & ", _to_string(options, model, cref), " \\\\") end end - _print_nonlinear_constraints(io, mime, model, nlp_block) + _print_nonlinear_constraints(io, options, model, nlp_block) return print(io, "\\end{aligned} \$\$") end @@ -440,7 +600,7 @@ inside a function. latex_formulation(model::MOI.ModelLike) = _LatexModel(model) function Base.show(io::IO, model::_LatexModel) - return _print_model(io, MIME("text/latex"), model.model) + return _print_model(io, _PrintOptions(MIME("text/latex")), model.model) end Base.show(io::IO, ::MIME"text/latex", model::_LatexModel) = show(io, model) @@ -452,9 +612,9 @@ function Base.print(model::MOI.ModelLike) return display(d, "text/latex", latex_formulation(model)) end end - return _print_model(stdout, MIME("text/plain"), model) + return print(stdout, model) end -function Base.print(io::IO, model::MOI.ModelLike) - return _print_model(io, MIME("text/plain"), model) +function Base.print(io::IO, model::MOI.ModelLike; kwargs...) + return _print_model(io, _PrintOptions(MIME("text/plain"); kwargs...), model) end diff --git a/test/Utilities/print.jl b/test/Utilities/print.jl index a57a497750..a878576cba 100644 --- a/test/Utilities/print.jl +++ b/test/Utilities/print.jl @@ -19,8 +19,32 @@ end function test_nonname_variable() model = MOIU.Model{Float64}() x = MOI.add_variable(model) - @test MOIU._to_string(PLAIN, model, x) == "noname" - @test MOIU._to_string(LATEX, model, x) == "noname" + @test MOIU._to_string(PLAIN, model, x) == "v[1]" + @test MOIU._to_string(LATEX, model, x) == "v_{1}" +end + +function test_numbers() + options = + MOIU._PrintOptions(MIME("text/plain"); simplify_coefficients = true) + @test MOIU._to_string(options, 1.0, "x"; is_first = true) == "x" + @test MOIU._to_string(options, 1.0, "x"; is_first = false) == " + x" + @test MOIU._to_string(options, -1.0, "x"; is_first = true) == "-x" + @test MOIU._to_string(options, -1.0, "x"; is_first = false) == " - x" + @test MOIU._to_string(options, 1.2, "x"; is_first = true) == "1.2 x" + @test MOIU._to_string(options, 1.2, "x"; is_first = false) == " + 1.2 x" + @test MOIU._to_string(options, -1.2, "x"; is_first = true) == "-1.2 x" + @test MOIU._to_string(options, -1.2, "x"; is_first = false) == " - 1.2 x" + + options = + MOIU._PrintOptions(MIME("text/plain"); simplify_coefficients = false) + @test MOIU._to_string(options, 1.0, "x"; is_first = true) == "1.0 x" + @test MOIU._to_string(options, 1.0, "x"; is_first = false) == " + 1.0 x" + @test MOIU._to_string(options, -1.0, "x"; is_first = true) == "-1.0 x" + @test MOIU._to_string(options, -1.0, "x"; is_first = false) == " - 1.0 x" + @test MOIU._to_string(options, 1.2, "x"; is_first = true) == "1.2 x" + @test MOIU._to_string(options, 1.2, "x"; is_first = false) == " + 1.2 x" + @test MOIU._to_string(options, -1.2, "x"; is_first = true) == "-1.2 x" + @test MOIU._to_string(options, -1.2, "x"; is_first = false) == " - 1.2 x" end function test_variable() @@ -47,10 +71,18 @@ function test_ScalarAffineTerm() model = MOIU.Model{Float64}() x = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") - @test MOIU._to_string(PLAIN, model, MOI.ScalarAffineTerm(-1.2, x)) == - " - 1.2 x" - @test MOIU._to_string(LATEX, model, MOI.ScalarAffineTerm(1.2, x)) == - " + 1.2 x" + @test MOIU._to_string( + PLAIN, + model, + MOI.ScalarAffineTerm(-1.2, x); + is_first = false, + ) == " - 1.2 x" + @test MOIU._to_string( + LATEX, + model, + MOI.ScalarAffineTerm(1.2, x); + is_first = false, + ) == " + 1.2 x" end function test_ScalarAffineFunction() @@ -72,11 +104,12 @@ function test_ScalarQuadraticTerm() MOI.set(model, MOI.VariableName(), x, "x") MOI.set(model, MOI.VariableName(), y, "y") term = MOI.ScalarQuadraticTerm(-1.2, x, x) - @test MOIU._to_string(PLAIN, model, term) == " - 0.6 x²" - @test MOIU._to_string(LATEX, model, term) == " - 0.6 x^2" + @test MOIU._to_string(PLAIN, model, term; is_first = false) == " - 0.6 x²" + @test MOIU._to_string(LATEX, model, term; is_first = false) == " - 0.6 x^2" term = MOI.ScalarQuadraticTerm(1.2, x, y) - @test MOIU._to_string(PLAIN, model, term) == " + 1.2 x*y" - @test MOIU._to_string(LATEX, model, term) == " + 1.2 x\\times y" + @test MOIU._to_string(PLAIN, model, term; is_first = false) == " + 1.2 x*y" + @test MOIU._to_string(LATEX, model, term; is_first = false) == + " + 1.2 x\\times y" end function test_ScalarQuadraticFunction() @@ -347,6 +380,138 @@ function test_latex() return end +function test_latex_simplified() + model = MOIU.Model{Float64}() + MOIU.loadfromstring!( + model, + """ + variables: x, y, z + minobjective: x + 2 + 3.1*y + -1.2*z + c1: x >= 0.1 + c2: y in ZeroOne() + c2: z in Integer() + c3: [x, y] in SecondOrderCone(2) + c4: [1, x, y] in SecondOrderCone(2) + c4: [1.0 * x * x, y, 1] in ExponentialCone() + c4: [1, 1.0 * x * x, y] in ExponentialCone() + c2: x in ZeroOne() + c5: 2.0 * x * x + y + -1 * z <= 1.0 + c5: x + x >= 1.0 + c5: x + x in Interval(1.0, 2.0) + c5: x + -1 * y == 0.0 + """, + ) + model_string = sprint() do io + return MOIU._print_model( + io, + MOIU._PrintOptions( + MIME("text/latex"); + simplify_coefficients = true, + print_constraint_types = false, + ), + model, + ) + end + _string_compare( + model_string, + raw""" + $$ \begin{aligned} + \min\quad & 2 + x + 3.1 y - 1.2 z \\ + \text{Subject to}\\ + & x - y = 0 \\ + & 2 x \ge 1 \\ + & 2 x \in \[1, 2\] \\ + & y - z + 2 x^2 \le 1 \\ + & \begin{bmatrix} + x\\ + y\end{bmatrix} \in \text{SecondOrderCone(2)} \\ + & \begin{bmatrix} + 1\\ + x\\ + y\end{bmatrix} \in \text{SecondOrderCone(2)} \\ + & \begin{bmatrix} + x^2\\ + y\\ + 1\end{bmatrix} \in \text{ExponentialCone()} \\ + & \begin{bmatrix} + 1\\ + x^2\\ + y\end{bmatrix} \in \text{ExponentialCone()} \\ + & x \ge 0.1 \\ + & z \in \mathbb{Z} \\ + & x \in \{0, 1\} \\ + & y \in \{0, 1\} \\ + \end{aligned} $$""", + ) + return +end + +function test_plain_simplified() + model = MOIU.Model{Float64}() + MOIU.loadfromstring!( + model, + """ + variables: x, y, z + minobjective: x + -2 + 3.1*y + -1.2*z + c1: x >= 0.1 + c2: y in ZeroOne() + c2: z in Integer() + c3: [x, y] in SecondOrderCone(2) + c4: [1, x, y] in SecondOrderCone(2) + c4: [1.0 * x * x, y, 1] in ExponentialCone() + c4: [1, 1.0 * x * x, y] in ExponentialCone() + c2: x in ZeroOne() + c5: 2.0 * x * x + y + -1 * z <= 1.0 + c5: x + x >= 1.0 + c5: x + x in Interval(1.0, 2.0) + c5: x + -1 * y == 0.0 + """, + ) + model_string = sprint() do io + return MOIU._print_model( + io, + MOIU._PrintOptions( + MIME("text/plain"); + simplify_coefficients = true, + print_constraint_types = false, + ), + model, + ) + end + @test model_string == """ + Minimize: -2 + x + 3.1 y - 1.2 z + + Subject to: + x - y == 0 + 2 x >= 1 + 2 x $(IN) [1, 2] + y - z + 2 x² <= 1 + ┌ ┐ + │x│ + │y│ + └ ┘ $(IN) SecondOrderCone(2) + ┌ ┐ + │1│ + │x│ + │y│ + └ ┘ $(IN) SecondOrderCone(2) + ┌ ┐ + │x²│ + │y │ + │1 │ + └ ┘ $(IN) ExponentialCone() + ┌ ┐ + │1 │ + │x²│ + │y │ + └ ┘ $(IN) ExponentialCone() + x >= 0.1 + z $(IN) ℤ + x $(IN) {0, 1} + y $(IN) {0, 1} + """ +end + function test_nlp() model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) v = MOI.add_variables(model, 4) From d4eb59abb7cc40c40075685a60a82c652e12ffe2 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 30 Apr 2021 12:00:17 +1200 Subject: [PATCH 09/11] Expose options --- src/Utilities/print.jl | 55 +++++++++++++++++++++++++++++++---------- test/Utilities/print.jl | 4 +-- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/Utilities/print.jl b/src/Utilities/print.jl index f23ed8d132..b6e2fc8229 100644 --- a/src/Utilities/print.jl +++ b/src/Utilities/print.jl @@ -5,18 +5,35 @@ _drop_moi(s) = replace(string(s), "MathOptInterface." => "") struct _PrintOptions{T<:MIME} simplify_coefficients::Bool default_name::String - print_constraint_types::Bool + print_types::Bool + + """ + _PrintOptions( + mime::MIME; + simplify_coefficients::Bool = false, + default_name::String = "v", + print_types::Bool = true, + ) + + A struct to control options for printing. + + ## Arguments + * `simplifiy_coefficients` : Simplify coefficients if possible by omitting + them or removing trailing zeros. + * `default_name` : The name given to variables with an empty name. + * `print_types` : Print the MOI type of each function and set for clarity. + """ function _PrintOptions( mime::MIME; simplify_coefficients::Bool = false, default_name::String = "v", - print_constraint_types::Bool = true, + print_types::Bool = true, ) return new{typeof(mime)}( simplify_coefficients, default_name, - print_constraint_types, + print_types, ) end end @@ -400,7 +417,7 @@ function _print_nonlinear_constraints( block::MOI.NLPBlockData, ) lookup = Dict{MOI.VariableIndex,_VariableNode}() - if options.print_constraint_types + if options.print_types println(io, "\nNonlinear") end has_expr = :ExprGraph in MOI.features_available(block.evaluator) @@ -421,7 +438,7 @@ function _print_nonlinear_constraints( block::MOI.NLPBlockData, ) lookup = Dict{MOI.VariableIndex,_VariableNode}() - if options.print_constraint_types + if options.print_types println(io, " & \\text{Nonlinear} \\\\") end has_expr = :ExprGraph in MOI.features_available(block.evaluator) @@ -522,7 +539,7 @@ function _print_model( else F, f = _objective_function_string(options, model, nlp_block) sense_s = sense == MOI.MIN_SENSE ? "Minimize" : "Maximize" - if options.print_constraint_types + if options.print_types println(io, sense_s, " ", F, ":\n ", f) else println(io, sense_s, ": ", f) @@ -530,7 +547,7 @@ function _print_model( end println(io, "\nSubject to:") for (F, S) in MOI.get(model, MOI.ListOfConstraints()) - if options.print_constraint_types + if options.print_types println(io, "\n$(_drop_moi(F))-in-$(_drop_moi(S))") end for cref in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) @@ -568,7 +585,7 @@ function _print_model( end println(io, "\\text{Subject to}\\\\") for (F, S) in MOI.get(model, MOI.ListOfConstraints()) - if options.print_constraint_types + if options.print_types println(io, " & \\text{$(_drop_moi(F))-in-$(_drop_moi(S))} \\\\") end for cref in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) @@ -585,10 +602,11 @@ end struct _LatexModel{T<:MOI.ModelLike} model::T + kwargs::Any end """ - latex_formulation(model::MOI.ModelLike) + latex_formulation(model::MOI.ModelLike; kwargs...) Wrap `model` in a type so that it can be pretty-printed as `text/latex` in a notebook like IJulia, or in Documenter. @@ -596,23 +614,34 @@ notebook like IJulia, or in Documenter. To render the model, end the cell with `latex_formulation(model)`, or call `display(latex_formulation(model))` in to force the display of the model from inside a function. + +Possible keyword arguments are: + + * `simplifiy_coefficients` : Simplify coefficients if possible by omitting + them or removing trailing zeros. + * `default_name` : The name given to variables with an empty name. + * `print_types` : Print the MOI type of each function and set for clarity. """ -latex_formulation(model::MOI.ModelLike) = _LatexModel(model) +latex_formulation(model::MOI.ModelLike; kwargs...) = _LatexModel(model, kwargs) function Base.show(io::IO, model::_LatexModel) - return _print_model(io, _PrintOptions(MIME("text/latex")), model.model) + return _print_model( + io, + _PrintOptions(MIME("text/latex"); model.kwargs...), + model.model, + ) end Base.show(io::IO, ::MIME"text/latex", model::_LatexModel) = show(io, model) -function Base.print(model::MOI.ModelLike) +function Base.print(model::MOI.ModelLike; kwargs...) for d in Base.Multimedia.displays if Base.Multimedia.displayable(d, "text/latex") && startswith("$(typeof(d))", "IJulia.") return display(d, "text/latex", latex_formulation(model)) end end - return print(stdout, model) + return print(stdout, model; kwargs...) end function Base.print(io::IO, model::MOI.ModelLike; kwargs...) diff --git a/test/Utilities/print.jl b/test/Utilities/print.jl index a878576cba..533b2047e6 100644 --- a/test/Utilities/print.jl +++ b/test/Utilities/print.jl @@ -407,7 +407,7 @@ function test_latex_simplified() MOIU._PrintOptions( MIME("text/latex"); simplify_coefficients = true, - print_constraint_types = false, + print_types = false, ), model, ) @@ -473,7 +473,7 @@ function test_plain_simplified() MOIU._PrintOptions( MIME("text/plain"); simplify_coefficients = true, - print_constraint_types = false, + print_types = false, ), model, ) From fd29a8279314570415ce3042c305657b86a9f70f Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 3 May 2021 08:02:46 +1200 Subject: [PATCH 10/11] Update print.jl --- src/Utilities/print.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Utilities/print.jl b/src/Utilities/print.jl index b6e2fc8229..b023c88157 100644 --- a/src/Utilities/print.jl +++ b/src/Utilities/print.jl @@ -19,7 +19,7 @@ struct _PrintOptions{T<:MIME} ## Arguments - * `simplifiy_coefficients` : Simplify coefficients if possible by omitting + * `simplify_coefficients` : Simplify coefficients if possible by omitting them or removing trailing zeros. * `default_name` : The name given to variables with an empty name. * `print_types` : Print the MOI type of each function and set for clarity. @@ -617,7 +617,7 @@ inside a function. Possible keyword arguments are: - * `simplifiy_coefficients` : Simplify coefficients if possible by omitting + * `simplify_coefficients` : Simplify coefficients if possible by omitting them or removing trailing zeros. * `default_name` : The name given to variables with an empty name. * `print_types` : Print the MOI type of each function and set for clarity. @@ -638,7 +638,7 @@ function Base.print(model::MOI.ModelLike; kwargs...) for d in Base.Multimedia.displays if Base.Multimedia.displayable(d, "text/latex") && startswith("$(typeof(d))", "IJulia.") - return display(d, "text/latex", latex_formulation(model)) + return display(d, "text/latex", latex_formulation(model; kwargs...)) end end return print(stdout, model; kwargs...) From d2087e627027f7406453e7d426db615509756bd3 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 3 May 2021 09:54:12 +1200 Subject: [PATCH 11/11] Update print.jl --- src/Utilities/print.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Utilities/print.jl b/src/Utilities/print.jl index b023c88157..5c0bee7fd7 100644 --- a/src/Utilities/print.jl +++ b/src/Utilities/print.jl @@ -177,7 +177,7 @@ function _to_string( term::MOI.ScalarAffineTerm; is_first::Bool, ) - name = _to_string(options, model, term.variable_index) + name = _to_string(options, model, term.variable) return _to_string(options, term.coefficient, name; is_first = is_first) end @@ -204,12 +204,12 @@ function _to_string( term::MOI.ScalarQuadraticTerm; is_first::Bool, ) - name_1 = _to_string(options, model, term.variable_index_1) - name_2 = _to_string(options, model, term.variable_index_2) + name_1 = _to_string(options, model, term.variable_1) + name_2 = _to_string(options, model, term.variable_2) # Be careful here when printing the coefficient. ScalarQuadraticFunction # assumes an additional 0.5 factor! coef = term.coefficient - name = if term.variable_index_1 == term.variable_index_2 + name = if term.variable_1 == term.variable_2 coef /= 2 string(name_1, _to_string(options, :sq)) else @@ -546,7 +546,7 @@ function _print_model( end end println(io, "\nSubject to:") - for (F, S) in MOI.get(model, MOI.ListOfConstraints()) + for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) if options.print_types println(io, "\n$(_drop_moi(F))-in-$(_drop_moi(S))") end @@ -584,7 +584,7 @@ function _print_model( println(io, "\\", sense_s, "\\quad & ", f, " \\\\") end println(io, "\\text{Subject to}\\\\") - for (F, S) in MOI.get(model, MOI.ListOfConstraints()) + for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) if options.print_types println(io, " & \\text{$(_drop_moi(F))-in-$(_drop_moi(S))} \\\\") end