diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 71e2eb5d96e..7bb78b08e0c 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -16,6 +16,8 @@ jobs: with: version: '1' - name: Install dependencies + env: + WLSLICENSE: ${{ secrets.WLSLICENSE }} shell: julia --color=yes --project=docs/ {0} run: | using Pkg @@ -23,6 +25,7 @@ jobs: Pkg.instantiate() - name: Build and deploy env: + WLSLICENSE: ${{ secrets.WLSLICENSE }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key DOCUMENTER_LATEX_DEBUG: ${{ github.workspace }}/latex-debug-logs @@ -31,7 +34,7 @@ jobs: if: ${{ always() }} with: name: PDF build logs - path: ${{ github.workspace }}/latex-debug-logs + path: ${{ github.workspace }}/latex-debug-logs - uses: errata-ai/vale-action@reviewdog with: version: 3.3.1 diff --git a/docs/Project.toml b/docs/Project.toml index 4577e5026f2..3c708a16bfa 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -14,7 +14,7 @@ Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" Dualization = "191a621a-6537-11e9-281d-650236a99e60" Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" -GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" +Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" @@ -60,7 +60,7 @@ DocumenterCitations = "1" Dualization = "0.5" Enzyme = "0.13.7" ForwardDiff = "0.10" -GLPK = "=1.2.1" +Gurobi = "1" HTTP = "1.5.4" HiGHS = "=1.12.0" Images = "0.26.1" diff --git a/docs/src/manual/callbacks.md b/docs/src/manual/callbacks.md index 0b9f3d377cf..b3da4313d52 100644 --- a/docs/src/manual/callbacks.md +++ b/docs/src/manual/callbacks.md @@ -89,9 +89,7 @@ information about lazy constraints, see this [blog post by Paul Rubin](https://o A lazy constraint callback can be set using the following syntax: ```jldoctest -julia> import GLPK - -julia> model = Model(GLPK.Optimizer); +julia> model = Model(); julia> @variable(model, x <= 10, Int) x @@ -117,6 +115,7 @@ julia> function my_callback_function(cb_data) con = @build_constraint(x <= 2) MOI.submit(model, MOI.LazyConstraint(cb_data), con) end + return end my_callback_function (generic function with 1 method) @@ -134,19 +133,21 @@ julia> set_attribute(model, MOI.LazyConstraintCallback(), my_callback_function) solver returning an incorrect solution, or lead to many constraints being added, slowing down the solution process. ```julia - model = Model(GLPK.Optimizer) + model = Model() @variable(model, x <= 10, Int) @objective(model, Max, x) function bad_callback_function(cb_data) # Don't do this! con = @build_constraint(x <= 2) MOI.submit(model, MOI.LazyConstraint(cb_data), con) + return end function good_callback_function(cb_data) if callback_value(x) > 2 con = @build_constraint(x <= 2) MOI.submit(model, MOI.LazyConstraint(cb_data), con) end + return end set_attribute(model, MOI.LazyConstraintCallback(), good_callback_function) ``` @@ -170,9 +171,7 @@ aforementioned [blog post](https://orinanobworld.blogspot.com/2012/08/user-cuts- A user-cut callback can be set using the following syntax: ```jldoctest -julia> import GLPK - -julia> model = Model(GLPK.Optimizer); +julia> model = Model(); julia> @variable(model, x <= 10.5, Int) x @@ -184,6 +183,7 @@ julia> function my_callback_function(cb_data) x_val = callback_value(cb_data, x) con = @build_constraint(x <= floor(x_val)) MOI.submit(model, MOI.UserCut(cb_data), con) + return end my_callback_function (generic function with 1 method) @@ -221,15 +221,11 @@ solutions from them. A heuristic solution callback can be set using the following syntax: ```jldoctest -julia> import GLPK +julia> model = Model(); -julia> model = Model(GLPK.Optimizer); +julia> @variable(model, x <= 10.5, Int); -julia> @variable(model, x <= 10.5, Int) -x - -julia> @objective(model, Max, x) -x +julia> @objective(model, Max, x); julia> function my_callback_function(cb_data) x_val = callback_value(cb_data, x) @@ -237,6 +233,7 @@ julia> function my_callback_function(cb_data) model, MOI.HeuristicSolution(cb_data), [x], [floor(Int, x_val)] ) println("I submitted a heuristic solution, and the status was: ", status) + return end my_callback_function (generic function with 1 method) diff --git a/docs/src/tutorials/algorithms/benders_decomposition.jl b/docs/src/tutorials/algorithms/benders_decomposition.jl index 8d25ead0979..bca7412ccf7 100644 --- a/docs/src/tutorials/algorithms/benders_decomposition.jl +++ b/docs/src/tutorials/algorithms/benders_decomposition.jl @@ -26,7 +26,7 @@ # in JuMP. It uses the following packages: using JuMP -import GLPK +import Gurobi import HiGHS import Printf import Test #src @@ -283,13 +283,13 @@ objective_value(model) # node of the first-stage MILP at each iteration. # !!! tip -# We use GLPK for this model because HiGHS does not support lazy constraints. -# For more information on callbacks, read the page +# We use Gurobi for this model because HiGHS does not support lazy +# constraints. For more information on callbacks, read the page # [Solver-independent callbacks](@ref callbacks_manual). # As before, we construct the same first-stage subproblem: -lazy_model = Model(GLPK.Optimizer) +lazy_model = Model(Gurobi.Optimizer) set_silent(lazy_model) @variable(lazy_model, x[1:n, 1:n], Bin) @variable(lazy_model, θ >= M) diff --git a/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl b/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl index 82dd8871eea..342f749fcd1 100644 --- a/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl +++ b/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl @@ -31,9 +31,10 @@ # It uses the following packages: using JuMP -import GLPK -import Random +import Gurobi import Plots +import Random +import Test # ## [Mathematical Formulation](@id tsp_model) @@ -124,7 +125,7 @@ function generate_distance_matrix(n; random_seed = 1) return X, Y, d end -n = 20 +n = 100 X, Y, d = generate_distance_matrix(n) # For the JuMP model, we first initialize the model object. Then, we create the @@ -133,7 +134,8 @@ X, Y, d = generate_distance_matrix(n) # constraints that `x[i, j] == x[j, i]`. function build_tsp_model(d, n) - model = Model(GLPK.Optimizer) + model = Model(Gurobi.Optimizer) + set_silent(model) @variable(model, x[1:n, 1:n], Bin, Symmetric) @objective(model, Min, sum(d .* x) / 2) @constraint(model, [i in 1:n], sum(x[i, :]) == 2) @@ -271,14 +273,14 @@ optimize!(lazy_model) @assert is_solved_and_feasible(lazy_model) objective_value(lazy_model) +#- + +time_lazy = solve_time(lazy_model) + # This finds the same optimal tour: plot_tour(X, Y, value.(lazy_model[:x])) -# Surprisingly, for this particular model with GLPK, the solution time is worse -# than the iterative method: - -time_lazy = solve_time(lazy_model) +# The solution time is faster than the iterative approach: -# In most other cases and solvers, however, the lazy time should be faster than -# the iterative method. +Test.@test time_lazy < time_iterated diff --git a/docs/src/tutorials/linear/callbacks.jl b/docs/src/tutorials/linear/callbacks.jl index b647337a507..e23193922eb 100644 --- a/docs/src/tutorials/linear/callbacks.jl +++ b/docs/src/tutorials/linear/callbacks.jl @@ -11,7 +11,7 @@ # The tutorial uses the following packages: using JuMP -import GLPK +import Gurobi import Random import Test @@ -29,7 +29,8 @@ import Test # An example using a lazy constraint callback. function example_lazy_constraint() - model = Model(GLPK.Optimizer) + model = Model(Gurobi.Optimizer) + set_silent(model) @variable(model, 0 <= x <= 2.5, Int) @variable(model, 0 <= y <= 2.5, Int) @objective(model, Max, y) @@ -57,6 +58,7 @@ function example_lazy_constraint() println("Adding $(con)") MOI.submit(model, MOI.LazyConstraint(cb_data), con) end + return end set_attribute(model, MOI.LazyConstraintCallback(), my_callback_function) optimize!(model) @@ -78,7 +80,11 @@ function example_user_cut_constraint() Random.seed!(1) N = 30 item_weights, item_values = rand(N), rand(N) - model = Model(GLPK.Optimizer) + model = Model(Gurobi.Optimizer) + set_silent(model) + ## Turn off "Cuts" parameter so that our new one must be called. In real + ## models, you should leave "Cuts" turned on. + set_attribute(model, "Cuts", 0) @variable(model, x[1:N], Bin) @constraint(model, sum(item_weights[i] * x[i] for i in 1:N) <= 10) @objective(model, Max, sum(item_values[i] * x[i] for i in 1:N)) @@ -115,7 +121,11 @@ function example_heuristic_solution() Random.seed!(1) N = 30 item_weights, item_values = rand(N), rand(N) - model = Model(GLPK.Optimizer) + model = Model(Gurobi.Optimizer) + set_silent(model) + ## Turn off "Heuristics" parameter so that our new one must be called. In + ## real models, you should leave "Heuristics" turned on. + set_attribute(model, "Heuristics", 0) @variable(model, x[1:N], Bin) @constraint(model, sum(item_weights[i] * x[i] for i in 1:N) <= 10) @objective(model, Max, sum(item_values[i] * x[i] for i in 1:N)) @@ -140,41 +150,57 @@ end example_heuristic_solution() -# ## GLPK solver-dependent callback +# ## Gurobi solver-dependent callback -# An example using GLPK's solver-dependent callback. +# An example using Gurobi's solver-dependent callback. function example_solver_dependent_callback() - model = Model(GLPK.Optimizer) + model = direct_model(Gurobi.Optimizer()) @variable(model, 0 <= x <= 2.5, Int) @variable(model, 0 <= y <= 2.5, Int) @objective(model, Max, y) - lazy_called = false - function my_callback_function(cb_data) - lazy_called = true - reason = GLPK.glp_ios_reason(cb_data.tree) - println("Called from reason = $(reason)") - if reason != GLPK.GLP_IROWGEN + cb_calls = Cint[] + function my_callback_function(cb_data, cb_where::Cint) + ## You can reference variables outside the function as normal + push!(cb_calls, cb_where) + ## You can select where the callback is run + if cb_where == Gurobi.GRB_CB_MIPNODE + ## You can query a callback attribute using GRBcbget + resultP = Ref{Cint}() + Gurobi.GRBcbget( + cb_data, + cb_where, + Gurobi.GRB_CB_MIPNODE_STATUS, + resultP, + ) + if resultP[] != Gurobi.GRB_OPTIMAL + return # Solution is something other than optimal. + end + elseif cb_where != Gurobi.GRB_CB_MIPSOL return end + ## Before querying `callback_value`, you must call: + Gurobi.load_callback_variable_primal(cb_data, cb_where) x_val = callback_value(cb_data, x) y_val = callback_value(cb_data, y) + ## You can submit solver-independent MathOptInterface attributes such as + ## lazy constraints, user-cuts, and heuristic solutions. if y_val - x_val > 1 + 1e-6 con = @build_constraint(y - x <= 1) - println("Adding $(con)") MOI.submit(model, MOI.LazyConstraint(cb_data), con) elseif y_val + x_val > 3 + 1e-6 - con = @build_constraint(y - x <= 1) - println("Adding $(con)") + con = @build_constraint(y + x <= 3) MOI.submit(model, MOI.LazyConstraint(cb_data), con) end + ## You can terminate the callback as follows: + Gurobi.GRBterminate(backend(model)) + return end - set_attribute(model, GLPK.CallbackFunction(), my_callback_function) + ## You _must_ set this parameter if using lazy constraints. + set_attribute(model, "LazyConstraints", 1) + set_attribute(model, Gurobi.CallbackFunction(), my_callback_function) optimize!(model) - Test.@test is_solved_and_feasible(model) - Test.@test lazy_called - Test.@test value(x) == 1 - Test.@test value(y) == 2 + Test.@test termination_status(model) == MOI.INTERRUPTED return end diff --git a/src/callbacks.jl b/src/callbacks.jl index 42d947216af..fb7fe268891 100644 --- a/src/callbacks.jl +++ b/src/callbacks.jl @@ -17,27 +17,33 @@ primal solution available from [`callback_value`](@ref) is integer feasible. ## Example ```jldoctest; filter=r"CALLBACK_NODE_STATUS_.+" -julia> import GLPK +julia> import Gurobi -julia> model = Model(GLPK.Optimizer); +julia> model = Model(Gurobi.Optimizer); +Set parameter WLSAccessID +Set parameter WLSSecret +Set parameter LicenseID to value 722777 +Set parameter GURO_PAR_SPECIAL +WLS license 722777 - registered to JuMP Development + +julia> set_silent(model) julia> @variable(model, x <= 10, Int); julia> @objective(model, Max, x); -julia> function my_callback_function(cb_data) +julia> function my_callback_function(cb_data, cb_where) status = callback_node_status(cb_data, model) - println("Status is: ", status) + if status == MOI.CALLBACK_NODE_STATUS_INTEGER + println("Status is: ", status) + end return end my_callback_function (generic function with 1 method) -julia> set_attribute(model, GLPK.CallbackFunction(), my_callback_function) +julia> set_attribute(model, Gurobi.CallbackFunction(), my_callback_function) julia> optimize!(model) -Status is: CALLBACK_NODE_STATUS_UNKNOWN -Status is: CALLBACK_NODE_STATUS_UNKNOWN -Status is: CALLBACK_NODE_STATUS_INTEGER Status is: CALLBACK_NODE_STATUS_INTEGER ``` """ @@ -68,28 +74,35 @@ Use [`callback_node_status`](@ref) to check whether a solution is available. ## Example ```jldoctest -julia> import GLPK +julia> import Gurobi + +julia> model = Model(Gurobi.Optimizer); +Set parameter WLSAccessID +Set parameter WLSSecret +Set parameter LicenseID to value 722777 +Set parameter GURO_PAR_SPECIAL +WLS license 722777 - registered to JuMP Development -julia> model = Model(GLPK.Optimizer); +julia> set_silent(model) julia> @variable(model, x <= 10, Int); julia> @objective(model, Max, x); -julia> function my_callback_function(cb_data) +julia> function my_callback_function(cb_data, cb_where) status = callback_node_status(cb_data, model) if status == MOI.CALLBACK_NODE_STATUS_INTEGER + Gurobi.load_callback_variable_primal(cb_data, cb_where) println("Solution is: ", callback_value(cb_data, x)) end return end my_callback_function (generic function with 1 method) -julia> set_attribute(model, GLPK.CallbackFunction(), my_callback_function) +julia> set_attribute(model, Gurobi.CallbackFunction(), my_callback_function) julia> optimize!(model) Solution is: 10.0 -Solution is: 10.0 ``` """ function callback_value(cb_data, x::GenericVariableRef)