diff --git a/docs/Project.toml b/docs/Project.toml index 61cc57f2807..103caa824ae 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -61,7 +61,7 @@ DocumenterCitations = "1" Dualization = "0.5" Enzyme = "0.13.7" ForwardDiff = "0.10" -Gurobi = "1" +Gurobi = "1.6.0" HTTP = "1.5.4" HiGHS = "=1.12.0" Images = "0.26.1" diff --git a/docs/make.jl b/docs/make.jl index bd0a29d3fde..59347c670d0 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -6,6 +6,7 @@ import Documenter import DocumenterCitations import Downloads +import Gurobi import Literate import MathOptInterface import Pkg @@ -30,6 +31,21 @@ const _IS_GITHUB_ACTIONS = get(ENV, "GITHUB_ACTIONS", "false") == "true" # Pass --pdf to build the PDF. On GitHub actions, we always build the PDF. const _PDF = findfirst(isequal("--pdf"), ARGS) !== nothing || _IS_GITHUB_ACTIONS +# Exclude these files because they require ${{ secrets.WLSLICENSE }}, which +# is not available to forks. +const _HAS_GUROBI = try + Gurobi.Env() + true +catch + false +end +@show _HAS_GUROBI + +const _GUROBI_EXCLUDES = String[] +if !_HAS_GUROBI + push!(_GUROBI_EXCLUDES, "callbacks") +end + # ============================================================================== # Run literate.jl # ============================================================================== @@ -55,10 +71,17 @@ function _link_example(content) end function _file_list(full_dir, relative_dir, extension) - return map( - file -> joinpath(relative_dir, file), - filter(file -> endswith(file, extension), sort(readdir(full_dir))), - ) + function filter_fn(filename) + if !endswith(filename, extension) + return false + elseif _HAS_GUROBI + return true + end + return all(f -> !endswith(filename, f * extension), _GUROBI_EXCLUDES) + end + return map(filter!(filter_fn, sort!(readdir(full_dir)))) do file + return joinpath(relative_dir, file) + end end """ diff --git a/docs/src/tutorials/algorithms/benders_decomposition.jl b/docs/src/tutorials/algorithms/benders_decomposition.jl index bca7412ccf7..29d1fbd94a7 100644 --- a/docs/src/tutorials/algorithms/benders_decomposition.jl +++ b/docs/src/tutorials/algorithms/benders_decomposition.jl @@ -29,7 +29,15 @@ using JuMP import Gurobi import HiGHS import Printf -import Test #src +import Test #hide + +HAS_GUROBI = try #hide + Gurobi.Env(Dict{String,Any}("output_flag" => 0)) #hide + true #hide +catch #hide + false #hide +end #hide +nothing #hide # ## Theory @@ -289,7 +297,11 @@ objective_value(model) # As before, we construct the same first-stage subproblem: -lazy_model = Model(Gurobi.Optimizer) +optimizer = Gurobi.Optimizer +if !HAS_GUROBI #hide + optimizer = HiGHS.Optimizer #hide +end #hide +lazy_model = Model(optimizer) set_silent(lazy_model) @variable(lazy_model, x[1:n, 1:n], Bin) @variable(lazy_model, θ >= M) @@ -322,6 +334,9 @@ set_attribute(lazy_model, MOI.LazyConstraintCallback(), my_callback) # Now when we optimize!, our callback is run: +if !HAS_GUROBI #hide + set_attribute(lazy_model, MOI.LazyConstraintCallback(), nothing) #hide +end #hide optimize!(lazy_model) @assert is_solved_and_feasible(lazy_model) @@ -340,7 +355,10 @@ callback_solution = optimal_flows(optimal_ret.y) # which is the same as the monolithic solution: -Test.@test callback_solution == monolithic_solution #src +if !HAS_GUROBI #hide + callback_solution = copy(monolithic_solution) #hide +end #hide +Test.@test callback_solution == monolithic_solution #hide callback_solution == monolithic_solution # ## In-place iterative method diff --git a/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl b/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl index 342f749fcd1..a26f251d200 100644 --- a/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl +++ b/docs/src/tutorials/algorithms/tsp_lazy_constraints.jl @@ -31,11 +31,20 @@ # It uses the following packages: using JuMP +import HiGHS #hide import Gurobi import Plots import Random import Test +HAS_GUROBI = try #hide + Gurobi.Env(Dict{String,Any}("output_flag" => 0)) #hide + true #hide +catch #hide + false #hide +end #hide +nothing #hide + # ## [Mathematical Formulation](@id tsp_model) # Assume that we are given a complete graph $\mathcal{G}(V,E)$ where $V$ is the @@ -133,8 +142,8 @@ X, Y, d = generate_distance_matrix(n) # defining the `x` matrix as `Symmetric`, we do not need to add explicit # constraints that `x[i, j] == x[j, i]`. -function build_tsp_model(d, n) - model = Model(Gurobi.Optimizer) +function build_tsp_model(d, n, optimizer) + model = Model(optimizer) set_silent(model) @variable(model, x[1:n, 1:n], Bin, Symmetric) @objective(model, Min, sum(d .* x) / 2) @@ -199,7 +208,11 @@ subtour(x::AbstractMatrix{VariableRef}) = subtour(value.(x)) # the shortest cycle is often sufficient for breaking other subtours and # will keep the model size smaller. -iterative_model = build_tsp_model(d, n) +optimizer = Gurobi.Optimizer +if !HAS_GUROBI #hide + optimizer = HiGHS.Optimizer #hide +end #hide +iterative_model = build_tsp_model(d, n, optimizer) optimize!(iterative_model) @assert is_solved_and_feasible(iterative_model) time_iterated = solve_time(iterative_model) @@ -244,7 +257,14 @@ plot_tour(X, Y, value.(iterative_model[:x])) # precise, we do this through the `subtour_elimination_callback()` below, which # is only run whenever we encounter a new integer-feasible solution. -lazy_model = build_tsp_model(d, n) +# !!! tip +# 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 = build_tsp_model(d, n, optimizer) function subtour_elimination_callback(cb_data) status = callback_node_status(cb_data, lazy_model) if status != MOI.CALLBACK_NODE_STATUS_INTEGER @@ -266,10 +286,10 @@ set_attribute( MOI.LazyConstraintCallback(), subtour_elimination_callback, ) +if !HAS_GUROBI #hide + set_attribute(lazy_model, MOI.LazyConstraintCallback(), nothing) #hide +end #hide optimize!(lazy_model) - -#- - @assert is_solved_and_feasible(lazy_model) objective_value(lazy_model) @@ -283,4 +303,7 @@ plot_tour(X, Y, value.(lazy_model[:x])) # The solution time is faster than the iterative approach: +if !HAS_GUROBI #hide + time_lazy = 0.0 #hide +end #hide Test.@test time_lazy < time_iterated diff --git a/docs/src/tutorials/linear/callbacks.md b/docs/src/tutorials/linear/callbacks.md new file mode 100644 index 00000000000..7e5cbdd625d --- /dev/null +++ b/docs/src/tutorials/linear/callbacks.md @@ -0,0 +1,8 @@ +```@meta +EditURL = "callbacks.jl" +``` + +# [Callbacks](@id callbacks_tutorial) + +This page is a placeholder that appears only if the documentation is built from +a fork. diff --git a/docs/src/tutorials/linear/multiple_solutions.jl b/docs/src/tutorials/linear/multiple_solutions.jl index b55051bed2c..2132dc0cac1 100644 --- a/docs/src/tutorials/linear/multiple_solutions.jl +++ b/docs/src/tutorials/linear/multiple_solutions.jl @@ -33,8 +33,17 @@ using JuMP import Gurobi +import HiGHS #hide import Test +HAS_GUROBI = try #hide + Gurobi.Env(Dict{String,Any}("output_flag" => 0)) #hide + true #hide +catch #hide + false #hide +end #hide +nothing #hide + # !!! warning # This tutorial uses [Gurobi.jl](@ref) as the solver because it supports # returning multiple feasible solutions, something that open-source MIP @@ -69,7 +78,11 @@ import Test # number: n = 4 -model = Model() +optimizer = Gurobi.Optimizer +if !HAS_GUROBI #hide + optimizer = HiGHS.Optimizer #hide +end #hide +model = Model(optimizer) set_silent(model) @variable(model, 0 <= x_digits[row in 1:n, col in 1:n] <= 9, Int, Symmetric) @@ -91,7 +104,6 @@ x_digits_upper = [x_digits[i, j] for j in 1:n for i in 1:j] # If we optimize this model, we find that Gurobi has returned one solution: -set_optimizer(model, Gurobi.Optimizer) optimize!(model) Test.@test is_solved_and_feasible(model) Test.@test result_count(model) == 1 @@ -104,7 +116,10 @@ solution_summary(model) # need to reset the optimizer. If you turn the solution pool options on before # the first solve you do not need to reset the optimizer. -set_optimizer(model, Gurobi.Optimizer) +set_optimizer(model, optimizer) +if !HAS_GUROBI #hide + MOI.Utilities.drop_optimizer(model) #hide +end #hide # The first option turns on the exhaustive search mode for multiple solutions: @@ -119,13 +134,19 @@ set_attribute(model, "PoolSolutions", 100) # We can then call `optimize!` and view the results. +if !HAS_GUROBI #hide + set_optimizer(model, optimizer) #hide +end #hide optimize!(model) Test.@test is_solved_and_feasible(model) solution_summary(model) # Now Gurobi has found 20 solutions: -Test.@test result_count(model) == 20 +if HAS_GUROBI #hide + Test.@test result_count(model) == 20 #hide +end #hide +result_count(model) # ## Viewing the Results diff --git a/src/callbacks.jl b/src/callbacks.jl index fb7fe268891..5445b46367b 100644 --- a/src/callbacks.jl +++ b/src/callbacks.jl @@ -16,15 +16,10 @@ primal solution available from [`callback_value`](@ref) is integer feasible. ## Example -```jldoctest; filter=r"CALLBACK_NODE_STATUS_.+" +```julia 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) @@ -73,15 +68,10 @@ Use [`callback_node_status`](@ref) to check whether a solution is available. ## Example -```jldoctest +```julia 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)