From 3e47a29a1e5a66904c338f87071b98c0ef287173 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 30 Dec 2024 12:40:15 +1300 Subject: [PATCH 1/6] [docs] add Gurobi to the docs and remove most usages of GLPK --- .github/workflows/documentation.yml | 5 +++- docs/Project.toml | 2 ++ docs/src/manual/callbacks.md | 25 ++++++++----------- .../algorithms/benders_decomposition.jl | 8 +++--- .../algorithms/tsp_lazy_constraints.jl | 22 ++++++++-------- 5 files changed, 33 insertions(+), 29 deletions(-) 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..f776f2f4700 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -15,6 +15,7 @@ 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" @@ -61,6 +62,7 @@ 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..46a1501ca59 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 = 50 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 From e5c8113f0c9e952cda2cdcb8fb646892f73daae7 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 30 Dec 2024 13:56:05 +1300 Subject: [PATCH 2/6] Update tsp_lazy_constraints.jl --- docs/src/tutorials/algorithms/tsp_lazy_constraints.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl b/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl index 46a1501ca59..342f749fcd1 100644 --- a/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl +++ b/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl @@ -125,7 +125,7 @@ function generate_distance_matrix(n; random_seed = 1) return X, Y, d end -n = 50 +n = 100 X, Y, d = generate_distance_matrix(n) # For the JuMP model, we first initialize the model object. Then, we create the From f16c4cc70c7b22da9198603c85d367b9cf0b4ac0 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 30 Dec 2024 14:11:57 +1300 Subject: [PATCH 3/6] Remove GLPK --- docs/Project.toml | 2 - docs/src/tutorials/linear/callbacks.jl | 68 ++++++++++++++++++-------- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index f776f2f4700..3c708a16bfa 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -14,7 +14,6 @@ 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" @@ -61,7 +60,6 @@ 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" diff --git a/docs/src/tutorials/linear/callbacks.jl b/docs/src/tutorials/linear/callbacks.jl index b647337a507..bd5f462190b 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 From caa56b1a91167eb9ea4a73a46ff6d42e877e4e55 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 30 Dec 2024 19:15:23 +1300 Subject: [PATCH 4/6] Update --- src/callbacks.jl | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/callbacks.jl b/src/callbacks.jl index 42d947216af..61334e8bdb7 100644 --- a/src/callbacks.jl +++ b/src/callbacks.jl @@ -17,28 +17,29 @@ 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); + +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) 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 +Status is: CALLBACK_NODE_STATUS_UNKNOWN ``` """ function callback_node_status(cb_data, model::GenericModel) @@ -68,28 +69,30 @@ 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); -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) From c267aed61f0cf928075e597aff289cad96892842 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 30 Dec 2024 21:20:52 +1300 Subject: [PATCH 5/6] Update callbacks.jl --- src/callbacks.jl | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/callbacks.jl b/src/callbacks.jl index 61334e8bdb7..fb7fe268891 100644 --- a/src/callbacks.jl +++ b/src/callbacks.jl @@ -20,6 +20,11 @@ primal solution available from [`callback_value`](@ref) is integer feasible. 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> set_silent(model) @@ -29,7 +34,9 @@ julia> @objective(model, Max, x); 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) @@ -37,9 +44,7 @@ my_callback_function (generic function with 1 method) julia> set_attribute(model, Gurobi.CallbackFunction(), my_callback_function) julia> optimize!(model) -Status is: CALLBACK_NODE_STATUS_UNKNOWN Status is: CALLBACK_NODE_STATUS_INTEGER -Status is: CALLBACK_NODE_STATUS_UNKNOWN ``` """ function callback_node_status(cb_data, model::GenericModel) @@ -72,6 +77,11 @@ Use [`callback_node_status`](@ref) to check whether a solution is available. 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> set_silent(model) From 489b09299b773a6921c53d86e1790e1970ef0650 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 31 Dec 2024 08:21:58 +1300 Subject: [PATCH 6/6] Update callbacks.jl --- docs/src/tutorials/linear/callbacks.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/src/tutorials/linear/callbacks.jl b/docs/src/tutorials/linear/callbacks.jl index bd5f462190b..e23193922eb 100644 --- a/docs/src/tutorials/linear/callbacks.jl +++ b/docs/src/tutorials/linear/callbacks.jl @@ -161,11 +161,11 @@ function example_solver_dependent_callback() @objective(model, Max, y) cb_calls = Cint[] function my_callback_function(cb_data, cb_where::Cint) - # You can reference variables outside the function as normal + ## You can reference variables outside the function as normal push!(cb_calls, cb_where) - # You can select where the callback is run + ## You can select where the callback is run if cb_where == Gurobi.GRB_CB_MIPNODE - # You can query a callback attribute using GRBcbget + ## You can query a callback attribute using GRBcbget resultP = Ref{Cint}() Gurobi.GRBcbget( cb_data, @@ -179,12 +179,12 @@ function example_solver_dependent_callback() elseif cb_where != Gurobi.GRB_CB_MIPSOL return end - # Before querying `callback_value`, you must call: + ## 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. + ## 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) MOI.submit(model, MOI.LazyConstraint(cb_data), con) @@ -192,11 +192,11 @@ function example_solver_dependent_callback() con = @build_constraint(y + x <= 3) MOI.submit(model, MOI.LazyConstraint(cb_data), con) end - # You can terminate the callback as follows: + ## You can terminate the callback as follows: Gurobi.GRBterminate(backend(model)) return end - # You _must_ set this parameter if using lazy constraints. + ## You _must_ set this parameter if using lazy constraints. set_attribute(model, "LazyConstraints", 1) set_attribute(model, Gurobi.CallbackFunction(), my_callback_function) optimize!(model)