diff --git a/src/nlp.jl b/src/nlp.jl index 6d1905ee293..77dfce21096 100644 --- a/src/nlp.jl +++ b/src/nlp.jl @@ -71,6 +71,20 @@ mutable struct NLPData evaluator end +""" + nlp_objective_function(model::Model) + +Returns the nonlinear objective function or `nothing` if no nonlinear objective +function is set. +""" +function nlp_objective_function(model::Model) + if model.nlp_data === nothing + return nothing + else + return model.nlp_data.nlobj + end +end + function create_nlp_block_data(m::Model) @assert m.nlp_data !== nothing bounds = MOI.NLPBoundsPair[] diff --git a/src/print.jl b/src/print.jl index 8ade8564ad4..8571cd461a0 100644 --- a/src/print.jl +++ b/src/print.jl @@ -143,11 +143,15 @@ end wrap_in_math_mode(str) = "\$\$ $str \$\$" wrap_in_inline_math_mode(str) = "\$ $str \$" +plural(n) = (n==1 ? "" : "s") + #------------------------------------------------------------------------ ## Model #------------------------------------------------------------------------ -function Base.show(io::IO, model::Model) - plural(n) = (n==1 ? "" : "s") + +# An `AbstractModel` subtype should implement `show_objective_function_summary`, +# `show_constraints_summary` and `show_backend_summary` for this method to work. +function Base.show(io::IO, model::AbstractModel) println(io, "A JuMP Model") sense = objective_sense(model) if sense == MOI.MAX_SENSE @@ -162,22 +166,19 @@ function Base.show(io::IO, model::Model) println(io, "Variable", plural(num_variables(model)), ": ", num_variables(model)) if sense != MOI.FEASIBILITY_SENSE - if model.nlp_data !== nothing && model.nlp_data.nlobj !== nothing - println(io, "Objective function type: Nonlinear") - else - println(io, "Objective function type: ", - MOI.get(model, MOI.ObjectiveFunctionType())) - end - end - for (F, S) in MOI.get(model, MOI.ListOfConstraints()) - num_constraints = MOI.get(model, MOI.NumberOfConstraints{F, S}()) - println(io, "`$F`-in-`$S`: $num_constraints constraint", - plural(num_constraints)) + show_objective_function_summary(io, model) end - if !iszero(num_nl_constraints(model)) - println(io, "Nonlinear: ", num_nl_constraints(model), " constraint", - plural(num_nl_constraints(model))) + show_constraints_summary(io, model) + show_backend_summary(io, model) + names_in_scope = sort(collect(keys(object_dictionary(model)))) + if !isempty(names_in_scope) + println(io) + print(io, "Names registered in the model: ", + join(string.(names_in_scope), ", ")) end +end + +function show_backend_summary(io::IO, model::Model) model_mode = mode(model) println(io, "Model mode: ", model_mode) if model_mode == MANUAL || model_mode == AUTOMATIC @@ -186,21 +187,18 @@ function Base.show(io::IO, model::Model) end # The last print shouldn't have a new line print(io, "Solver name: ", solver_name(model)) - names_in_scope = sort(collect(keys(object_dictionary(model)))) - if !isempty(names_in_scope) - println(io) - print(io, "Names registered in the model: ", - join(string.(names_in_scope), ", ")) - end end -function Base.print(io::IO, model::Model) +function Base.print(io::IO, model::AbstractModel) print(io, model_string(REPLMode, model)) end -function Base.show(io::IO, ::MIME"text/latex", model::Model) +function Base.show(io::IO, ::MIME"text/latex", model::AbstractModel) print(io, wrap_in_math_mode(model_string(IJuliaMode, model))) end -function model_string(print_mode, model::Model) + +# An `AbstractModel` subtype should implement `objective_function_string` and +# `constraints_string` for this method to work. +function model_string(print_mode, model::AbstractModel) ijl = print_mode == IJuliaMode sep = ijl ? " & " : " " eol = ijl ? "\\\\\n" : "\n" @@ -218,36 +216,46 @@ function model_string(print_mode, model::Model) str *= "\\quad" end str *= sep - if model.nlp_data !== nothing && model.nlp_data.nlobj !== nothing - str *= nl_expr_string(model, print_mode, model.nlp_data.nlobj) - else - str *= function_string(print_mode, - objective_function(model, QuadExpr)) - end + str *= objective_function_string(print_mode, model) end str *= eol str *= ijl ? "\\text{Subject to} \\quad" : "Subject to" * eol - for (F, S) in MOI.get(model, MOI.ListOfConstraints()) - for idx in MOI.get(model, MOI.ListOfConstraintIndices{F, S}()) - # FIXME the shape may be incorrect here - shape = S <: MOI.AbstractScalarSet ? ScalarShape() : VectorShape() - cref = ConstraintRef(model, idx, shape) - con = constraint_object(cref) - str *= sep * constraint_string(print_mode, con) * eol - end - end - if model.nlp_data !== nothing - for nl_constraint in model.nlp_data.nlconstr - str *= sep * nl_constraint_string(model, print_mode, nl_constraint) - str *= eol - end - end + str *= constraints_string(print_mode, model, sep, eol) if ijl str = "\\begin{alignat*}{1}" * str * "\\end{alignat*}\n" end return str end +""" + show_objective_function_summary(io::IO, model::AbstractModel) + +Write to `io` a summary of the objective function type. +""" +function show_objective_function_summary(io::IO, model::Model) + nlobj = nlp_objective_function(model) + print(io, "Objective function type: ") + if nlobj === nothing + println(io, objective_function_type(model)) + else + println(io, "Nonlinear") + end +end + +""" + objective_function_string(print_mode, model::AbstractModel)::String + +Return a `String` describing the objective function of the model. +""" +function objective_function_string(print_mode, model::Model) + nlobj = nlp_objective_function(model) + if nlobj === nothing + return function_string(print_mode, objective_function(model)) + else + return nl_expr_string(model, print_mode, nlobj) + end +end + #------------------------------------------------------------------------ ## VariableRef #------------------------------------------------------------------------ @@ -370,6 +378,49 @@ end ## Constraints #------------------------------------------------------------------------ +""" + show_constraints_summary(io::IO, model::AbstractModel) + +Write to `io` a summary of the number of constraints. +""" +function show_constraints_summary(io::IO, model::Model) + for (F, S) in MOI.get(model, MOI.ListOfConstraints()) + num_constraints = MOI.get(model, MOI.NumberOfConstraints{F, S}()) + println(io, "`$F`-in-`$S`: $num_constraints constraint", + plural(num_constraints)) + end + if !iszero(num_nl_constraints(model)) + println(io, "Nonlinear: ", num_nl_constraints(model), " constraint", + plural(num_nl_constraints(model))) + end +end + +""" + constraints_string(print_mode, model::AbstractModel, sep, eol)::String + +Return a `String` describing the constraints of the model, each on a line +starting with `sep` and ending with `eol` (which already contains `\n`). +""" +function constraints_string(print_mode, model::Model, sep, eol) + str = "" + for (F, S) in MOI.get(model, MOI.ListOfConstraints()) + for idx in MOI.get(model, MOI.ListOfConstraintIndices{F, S}()) + # FIXME the shape may be incorrect here + shape = S <: MOI.AbstractScalarSet ? ScalarShape() : VectorShape() + cref = ConstraintRef(model, idx, shape) + con = constraint_object(cref) + str *= sep * constraint_string(print_mode, con) * eol + end + end + if model.nlp_data !== nothing + for nl_constraint in model.nlp_data.nlconstr + str *= sep * nl_constraint_string(model, print_mode, nl_constraint) + str *= eol + end + end + return str +end + ## Notes for extensions # For a `ConstraintRef{ModelType, IndexType}` where `ModelType` is not # `JuMP.Model` or `IndexType` is not `MathOptInterface.ConstraintIndex`, the diff --git a/test/JuMPExtension.jl b/test/JuMPExtension.jl index 0737332c18f..d041f77f9a4 100644 --- a/test/JuMPExtension.jl +++ b/test/JuMPExtension.jl @@ -299,4 +299,27 @@ function JuMP.constraint_by_name(model::MyModel, name::String) end end +# Show +function JuMP.show_backend_summary(io::IO, model::MyModel) end +function JuMP.show_objective_function_summary(io::IO, model::MyModel) + println(io, "Objective function type: ", + JuMP.objective_function_type(model)) +end +function JuMP.objective_function_string(print_mode, model::MyModel) + return JuMP.function_string(print_mode, JuMP.objective_function(model)) +end +function JuMP.show_constraints_summary(io::IO, model::MyModel) + n = length(model.constraints) + print(io, "Constraint", JuMP.plural(n), ": ", n) +end +function JuMP.constraints_string(print_mode, model::MyModel, sep, eol) + str = "" + # Sort by creation order, i.e. ConstraintIndex value + constraints = sort(collect(model.constraints), by = c -> c.first.value) + for (index, constraint) in constraints + str *= sep * JuMP.constraint_string(print_mode, constraint) * eol + end + return str +end + end diff --git a/test/print.jl b/test/print.jl index a945f6c57c5..c48d033802e 100644 --- a/test/print.jl +++ b/test/print.jl @@ -266,18 +266,6 @@ function printing_test(ModelType::Type{<:JuMP.AbstractModel}) io_test(IJuliaMode, w[1,3], "symm_{1,3}") end - @testset "SingleVariable constraints" begin - ge = JuMP.math_symbol(REPLMode, :geq) - in_sym = JuMP.math_symbol(REPLMode, :in) - model = ModelType() - @variable(model, x >= 10) - zero_one = @constraint(model, x in MathOptInterface.ZeroOne()) - - io_test(REPLMode, JuMP.LowerBoundRef(x), "x $ge 10.0") - io_test(REPLMode, zero_one, "x binary") - # TODO: Test in IJulia mode - end - @testset "VectorOfVariable constraints" begin ge = JuMP.math_symbol(REPLMode, :geq) in_sym = JuMP.math_symbol(REPLMode, :in) @@ -348,6 +336,11 @@ function printing_test(ModelType::Type{<:JuMP.AbstractModel}) io_test(REPLMode, quad_constr, "2 x$sq $le 1.0") # TODO: Test in IJulia mode. end +end + +# Test printing of models of type `ModelType` for which the model is stored in +# an MOI backend +function model_printing_test(ModelType::Type{<:JuMP.AbstractModel}) @testset "Model" begin repl(s) = JuMP.math_symbol(REPLMode, s) le, ge, eq, fa = repl(:leq), repl(:geq), repl(:eq), repl(:for_all) @@ -375,6 +368,8 @@ function printing_test(ModelType::Type{<:JuMP.AbstractModel}) @constraint(model_1, a*b <= 2) @constraint(model_1, [1 - a; u] in SecondOrderCone()) + VariableType = typeof(a) + io_test(REPLMode, model_1, """ Max a - b + 2 a1 - 10 x Subject to @@ -400,12 +395,11 @@ function printing_test(ModelType::Type{<:JuMP.AbstractModel}) [-a + 1, u[1], u[2], u[3]] $inset MathOptInterface.SecondOrderCone(4) """, repl=:print) - io_test(REPLMode, model_1, """ A JuMP Model Maximization problem with: Variables: 13 - Objective function type: MathOptInterface.ScalarAffineFunction{Float64} + Objective function type: JuMP.GenericAffExpr{Float64,$VariableType} `MathOptInterface.SingleVariable`-in-`MathOptInterface.ZeroOne`: 4 constraints `MathOptInterface.SingleVariable`-in-`MathOptInterface.Integer`: 4 constraints `MathOptInterface.SingleVariable`-in-`MathOptInterface.EqualTo{Float64}`: 1 constraint @@ -463,11 +457,11 @@ function printing_test(ModelType::Type{<:JuMP.AbstractModel}) Solver name: No optimizer attached. Names registered in the model: x, y""", repl=:show) - model_2 = ModelType() - @variable(model_2, x) - @constraint(model_2, x <= 3) + model_3 = ModelType() + @variable(model_3, x) + @constraint(model_3, x <= 3) - io_test(REPLMode, model_2, """ + io_test(REPLMode, model_3, """ A JuMP Model Feasibility problem with: Variable: 1 @@ -479,8 +473,95 @@ function printing_test(ModelType::Type{<:JuMP.AbstractModel}) end end +# Test printing of models of type `ModelType` for which the model is stored in +# its JuMP form, e.g., as `AbstractVariable`s and `AbstractConstraint`s. +# This is used by `JuMPExtension` but can also be used by external packages such +# as `StructJuMP`, see https://github.com/JuliaOpt/JuMP.jl/issues/1711 +function model_extension_printing_test(ModelType::Type{<:JuMP.AbstractModel}) + @testset "Model" begin + repl(s) = JuMP.math_symbol(REPLMode, s) + le, ge, eq, fa = repl(:leq), repl(:geq), repl(:eq), repl(:for_all) + inset, dots = repl(:in), repl(:dots) + infty, union = repl(:infty), repl(:union) + Vert, sub2 = repl(:Vert), repl(:sub2) + for_all = repl(:for_all) + + #------------------------------------------------------------------ + + model_1 = ModelType() + @variable(model_1, a>=1) + @variable(model_1, b<=1) + @variable(model_1, -1<=c<=1) + @variable(model_1, a1>=1,Int) + @variable(model_1, b1<=1,Int) + @variable(model_1, -1<=c1<=1,Int) + @variable(model_1, x, Bin) + @variable(model_1, y) + @variable(model_1, z, Int) + @variable(model_1, u[1:3], Bin) + @variable(model_1, fi == 9) + @objective(model_1, Max, a - b + 2a1 - 10x) + @constraint(model_1, a + b - 10c - 2x + c1 <= 1) + @constraint(model_1, a*b <= 2) + @constraint(model_1, [1 - a; u] in SecondOrderCone()) + + VariableType = typeof(a) + + # TODO variable constraints + io_test(REPLMode, model_1, """ + Max a - b + 2 a1 - 10 x + Subject to + a + b - 10 c - 2 x + c1 $le 1.0 + a*b $le 2.0 + [-a + 1, u[1], u[2], u[3]] $inset MathOptInterface.SecondOrderCone(4) + """, repl=:print) + + io_test(REPLMode, model_1, """ + A JuMP Model + Maximization problem with: + Variables: 13 + Objective function type: JuMP.GenericAffExpr{Float64,$VariableType} + Constraints: 3 + Names registered in the model: a, a1, b, b1, c, c1, fi, u, x, y, z""", repl=:show) + + io_test(IJuliaMode, model_1, """ + \\begin{alignat*}{1}\\max\\quad & a - b + 2 a1 - 10 x\\\\ + \\text{Subject to} \\quad & a + b - 10 c - 2 x + c1 \\leq 1.0\\\\ + & a\\times b \\leq 2.0\\\\ + & [-a + 1, u_{1}, u_{2}, u_{3}] \\in MathOptInterface.SecondOrderCone(4)\\\\ + \\end{alignat*} + """) + + #------------------------------------------------------------------ + + model_2 = ModelType() + @variable(model_2, x, Bin) + @variable(model_2, y, Int) + @constraint(model_2, x*y <= 1) + + io_test(REPLMode, model_2, """ + A JuMP Model + Feasibility problem with: + Variables: 2 + Constraint: 1 + Names registered in the model: x, y""", repl=:show) + + model_3 = ModelType() + @variable(model_3, x) + @constraint(model_3, x <= 3) + + io_test(REPLMode, model_3, """ + A JuMP Model + Feasibility problem with: + Variable: 1 + Constraint: 1 + Names registered in the model: x""", repl=:show) + end +end + @testset "Printing for JuMP.Model" begin printing_test(Model) + model_printing_test(Model) @testset "Model with nonlinear terms" begin eq = JuMP.math_symbol(REPLMode, :eq) model = Model() @@ -511,9 +592,20 @@ end \\end{alignat*} """) end + @testset "SingleVariable constraints" begin + ge = JuMP.math_symbol(REPLMode, :geq) + in_sym = JuMP.math_symbol(REPLMode, :in) + model = Model() + @variable(model, x >= 10) + zero_one = @constraint(model, x in MathOptInterface.ZeroOne()) + + io_test(REPLMode, JuMP.LowerBoundRef(x), "x $ge 10.0") + io_test(REPLMode, zero_one, "x binary") + # TODO: Test in IJulia mode + end end -# TODO: These tests are failing. -# @testset "Printing for JuMPExtension.MyModel" begin -# printing_test(JuMPExtension.MyModel) -# end +@testset "Printing for JuMPExtension.MyModel" begin + printing_test(JuMPExtension.MyModel) + model_extension_printing_test(JuMPExtension.MyModel) +end