From f83ecde02c6bbfa3d6e0cec45340c3100011a634 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 5 Jan 2026 16:28:57 +1300 Subject: [PATCH 1/9] [docs] update to Julia v1.12 --- .github/workflows/documentation.yml | 4 ++-- docs/.gitignore | 1 - docs/Project.toml | 22 +++++++++++----------- docs/packages.toml | 14 +++++++------- docs/src/manual/nlp.md | 2 +- docs/src/manual/nonlinear.md | 8 ++++---- src/JuMP.jl | 2 +- src/macros/@force_nonlinear.jl | 4 ++-- 8 files changed, 28 insertions(+), 29 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index f9230242014..58344cc7eae 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -16,10 +16,10 @@ jobs: with: # Fix the Julia version, because Enzyme doesn't always like being updated # to `latest`. - version: '1.11' + version: '1.12' - uses: julia-actions/cache@v2 - name: Install Gurobi license - env: + env: SECRET_GUROBI_LICENSE: ${{ secrets.GUROBI_LICENSE }} shell: bash run: | diff --git a/docs/.gitignore b/docs/.gitignore index f389a3b10a2..f6c07c64ea7 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -9,5 +9,4 @@ src/release_notes.md src/packages/*.md !src/packages/solvers.md !src/packages/extensions.md -!src/packages/NLopt.md src/JuMP.pdf diff --git a/docs/Project.toml b/docs/Project.toml index 3adbbe83c41..bb9a02ae39c 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -54,33 +54,33 @@ Clarabel = "=0.11.0" Clustering = "0.15.8" DSP = "0.8.4" DataFrames = "1.8.1" -DifferentiationInterface = "0.7.12" +DifferentiationInterface = "0.7.13" DimensionalData = "=0.29.25" -Distributions = "0.25.122" -Documenter = "=1.15.0" +Distributions = "0.25.123" +Documenter = "1.16.1" DocumenterCitations = "1.4.1" Dualization = "0.7.1" -Enzyme = "0.13.109" -ForwardDiff = "1.3.0" -Gurobi = "=1.9.0" +Enzyme = "0.13.112" +ForwardDiff = "1.3.1" +Gurobi = "=1.9.1" HTTP = "1.10.19" HiGHS = "=1.20.1" Images = "0.26.2" -Interpolations = "0.15" +Interpolations = "0.16.2" Ipopt = "=1.13.0" JSON = "0.21.4" JSONSchema = "1.5.0" -LinearOperatorCollection = "2.2.1" +LinearOperatorCollection = "2.3.0" Literate = "2.21.0" MarkdownAST = "0.1.2" MathOptChordalDecomposition = "=0.2.0" MathOptInterface = "=1.48.0" -MultiObjectiveAlgorithms = "=1.8.0" +MultiObjectiveAlgorithms = "=1.8.1" PATHSolver = "=1.7.9" ParametricOptInterface = "0.14.1" -Plots = "1.41.2" +Plots = "1.41.3" RegularizedLeastSquares = "0.16.11" -SCS = "=2.4.0" +SCS = "=2.5.0" SQLite = "1.6.1" SpecialFunctions = "2.6.1" StatsPlots = "0.15.8" diff --git a/docs/packages.toml b/docs/packages.toml index 46b6919a3a1..e83b87f7832 100644 --- a/docs/packages.toml +++ b/docs/packages.toml @@ -47,7 +47,7 @@ [CSDP] rev = "v1.1.2" [cuOpt] - rev = "v0.1.1" + rev = "v0.1.2" [DiffOpt] rev = "v0.5.4" extension = true @@ -61,7 +61,7 @@ [GLPK] rev = "v1.2.1" [Gurobi] - rev = "v1.9.0" + rev = "v1.9.1" [HiGHS] rev = "v1.20.1" [Hypatia] @@ -70,7 +70,7 @@ [Ipopt] rev = "v1.13.0" [KNITRO] - rev = "v1.1.0" + rev = "v1.2.0" [MathOptAnalyzer] rev = "v0.1.1" [MathOptIIS] @@ -80,7 +80,7 @@ [MosekTools] rev = "v0.15.10" [MultiObjectiveAlgorithms] - rev = "3bf253221cfa943532b5030ab7d5bed6157b9786" + rev = "v1.8.1" has_html = true [NEOSServer] rev = "v1.2.0" @@ -103,7 +103,7 @@ rev = "v0.7.6" extension = true [SCS] - rev = "v2.4.0" + rev = "v2.5.0" [SDPA] rev = "v0.6.1" [SDPLR] @@ -144,7 +144,7 @@ has_html = true [COPT] user = "COPT-Public" - rev = "v1.1.30" + rev = "v1.1.31" [COSMO] user = "oxfordcontrol" rev = "v0.8.9" @@ -159,7 +159,7 @@ extension = true [EAGO] user = "PSORLab" - rev = "v0.8.3" + rev = "v0.8.4" filename = "docs/src/jump/README.md" [GAMS] user = "GAMS-dev" diff --git a/docs/src/manual/nlp.md b/docs/src/manual/nlp.md index b7a7c5fb1a6..6ff89b8265c 100644 --- a/docs/src/manual/nlp.md +++ b/docs/src/manual/nlp.md @@ -227,7 +227,7 @@ julia> @variable(model, y); julia> c = [1, 2]; julia> @NLobjective(model, Min, c' * x + 3y) -ERROR: Unexpected array [1 2] in nonlinear expression. Nonlinear expressions may contain only scalar expressions. +ERROR: Unexpected array adjoint([1, 2]) in nonlinear expression. Nonlinear expressions may contain only scalar expressions. [...] ``` diff --git a/docs/src/manual/nonlinear.md b/docs/src/manual/nonlinear.md index 8281b5b2a74..383be6bebf2 100644 --- a/docs/src/manual/nonlinear.md +++ b/docs/src/manual/nonlinear.md @@ -796,7 +796,7 @@ works. The operator takes `f(x::Vector)` as input, instead of the splatted `f(x...)`. -```jldoctest +```jldoctest; filter=r"\(::ForwardDiff.+\)" julia> import ForwardDiff julia> my_operator_bad(x::Vector) = sum(x[i]^2 for i in eachindex(x)) @@ -806,7 +806,7 @@ julia> my_operator_good(x...) = sum(x[i]^2 for i in eachindex(x)) my_operator_good (generic function with 1 method) julia> ForwardDiff.gradient(x -> my_operator_bad(x...), [1.0, 2.0]) -ERROR: MethodError: no method matching my_operator_bad(::ForwardDiff.Dual{ForwardDiff.Tag{var"#5#6", Float64}, Float64, 2}, ::ForwardDiff.Dual{ForwardDiff.Tag{var"#5#6", Float64}, Float64, 2}) +ERROR: MethodError: no method matching my_operator_bad(::ForwardDiff.Dual, ::ForwardDiff.Dual) [...] julia> ForwardDiff.gradient(x -> my_operator_good(x...), [1.0, 2.0]) @@ -820,7 +820,7 @@ julia> ForwardDiff.gradient(x -> my_operator_good(x...), [1.0, 2.0]) The operator assumes `Float64` will be passed as input, but it must work for any generic `Real` type. -```jldoctest +```jldoctest; filter=r"\(::ForwardDiff.+\)" julia> import ForwardDiff julia> my_operator_bad(x::Float64...) = sum(x[i]^2 for i in eachindex(x)) @@ -830,7 +830,7 @@ julia> my_operator_good(x::Real...) = sum(x[i]^2 for i in eachindex(x)) my_operator_good (generic function with 1 method) julia> ForwardDiff.gradient(x -> my_operator_bad(x...), [1.0, 2.0]) -ERROR: MethodError: no method matching my_operator_bad(::ForwardDiff.Dual{ForwardDiff.Tag{var"#5#6", Float64}, Float64, 2}, ::ForwardDiff.Dual{ForwardDiff.Tag{var"#5#6", Float64}, Float64, 2}) +ERROR: MethodError: no method matching my_operator_bad(::ForwardDiff.Dual, ::ForwardDiff.Dual) [...] julia> ForwardDiff.gradient(x -> my_operator_good(x...), [1.0, 2.0]) diff --git a/src/JuMP.jl b/src/JuMP.jl index 54f5edb5171..98a37e60aa8 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -1131,7 +1131,7 @@ julia> set_optimize_hook(model, my_hook) my_hook (generic function with 1 method) julia> optimize!(model; test_arg = true) -Base.Pairs{Symbol, Bool, Tuple{Symbol}, @NamedTuple{test_arg::Bool}}(:test_arg => 1) +Base.Pairs{Symbol, Bool, Nothing, @NamedTuple{test_arg::Bool}}(:test_arg => 1) Calling with `ignore_optimize_hook = true` ERROR: NoOptimizer() [...] diff --git a/src/macros/@force_nonlinear.jl b/src/macros/@force_nonlinear.jl index 0a1645d3d9a..e7fbf9d2d18 100644 --- a/src/macros/@force_nonlinear.jl +++ b/src/macros/@force_nonlinear.jl @@ -84,10 +84,10 @@ julia> @expression(model, @force_nonlinear(x * 2.0 * (1 + x) * x)) x * 2 * (1 + x) * x julia> @allocated @expression(model, x * 2.0 * (1 + x) * x) -3680 +3264 julia> @allocated @expression(model, @force_nonlinear(x * 2.0 * (1 + x) * x)) -768 +944 ``` """ macro force_nonlinear(expr) From 85be3943a9fcde2a01fe15edb47d92e730fa87de Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 6 Jan 2026 08:58:28 +1300 Subject: [PATCH 2/9] Update --- docs/Project.toml | 2 +- docs/src/manual/containers.md | 4 +- docs/src/tutorials/algorithms/pdhg.jl | 2 +- docs/src/tutorials/linear/callbacks.md | 216 ++++++++++++++++++++++++- 4 files changed, 218 insertions(+), 6 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index bb9a02ae39c..cb5e786413f 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -57,7 +57,7 @@ DataFrames = "1.8.1" DifferentiationInterface = "0.7.13" DimensionalData = "=0.29.25" Distributions = "0.25.123" -Documenter = "1.16.1" +Documenter = "=1.15.0" DocumenterCitations = "1.4.1" Dualization = "0.7.1" Enzyme = "0.13.112" diff --git a/docs/src/manual/containers.md b/docs/src/manual/containers.md index ef2cc42a2ed..999b3c09a6e 100644 --- a/docs/src/manual/containers.md +++ b/docs/src/manual/containers.md @@ -138,7 +138,7 @@ julia> DataFrames.DataFrame(table) 6 │ 2 3 (2, 3) ``` -## DenseAxisArray +## ## [DenseAxisArray](@id manual_dense_axis_array) A [`Containers.DenseAxisArray`](@ref) is created when the index sets are rectangular, but not of the form `1:n`. The index sets can be of any type. @@ -275,7 +275,7 @@ And data, a 2-element Vector{Tuple{Int64, Symbol}}: (2, :B) ``` -## SparseAxisArray +## [SparseAxisArray](@id manual_sparse_axis_array) A [`Containers.SparseAxisArray`](@ref) is created when the index sets are non-rectangular. This occurs in two circumstances: diff --git a/docs/src/tutorials/algorithms/pdhg.jl b/docs/src/tutorials/algorithms/pdhg.jl index 3f93e2affcd..f681af40855 100644 --- a/docs/src/tutorials/algorithms/pdhg.jl +++ b/docs/src/tutorials/algorithms/pdhg.jl @@ -255,7 +255,7 @@ end MOI.get(::Optimizer, ::MOI.SolverName) = "PDHG" -# ### GenericModel +# ### [GenericModel](@id tutorial_pdhg_generic_model) # The simplest way to solve a problem with your optimizer is to implement the # method `MOI.optimize!(dest::Optimizer, src::MOI.ModelLike)`, where `src` is an diff --git a/docs/src/tutorials/linear/callbacks.md b/docs/src/tutorials/linear/callbacks.md index 7e5cbdd625d..aa444b4d201 100644 --- a/docs/src/tutorials/linear/callbacks.md +++ b/docs/src/tutorials/linear/callbacks.md @@ -4,5 +4,217 @@ EditURL = "callbacks.jl" # [Callbacks](@id callbacks_tutorial) -This page is a placeholder that appears only if the documentation is built from -a fork. +_This tutorial was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl)._ +[_Download the source as a `.jl` file_](callbacks.jl). + +The purpose of the tutorial is to demonstrate the various solver-independent +and solver-dependent callbacks that are supported by JuMP. + +The tutorial uses the following packages: + +````@example callbacks +using JuMP +import Gurobi +import Random +import Test +```` + +!!! info + This tutorial uses the [MathOptInterface](@ref moi_documentation) API. + By default, JuMP exports the `MOI` symbol as an alias for the + MathOptInterface.jl package. We recommend making this more explicit in + your code by adding the following lines: + ```julia + import MathOptInterface as MOI + ``` + +## Lazy constraints + +An example using a lazy constraint callback. + +````@example callbacks +function example_lazy_constraint() + 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) + lazy_called = false + function my_callback_function(cb_data) + lazy_called = true + x_val = callback_value(cb_data, x) + y_val = callback_value(cb_data, y) + println("Called from (x, y) = ($x_val, $y_val)") + status = callback_node_status(cb_data, model) + if status == MOI.CALLBACK_NODE_STATUS_FRACTIONAL + println(" - Solution is integer infeasible!") + elseif status == MOI.CALLBACK_NODE_STATUS_INTEGER + println(" - Solution is integer feasible!") + else + @assert status == MOI.CALLBACK_NODE_STATUS_UNKNOWN + println(" - I don't know if the solution is integer feasible :(") + end + 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 <= 3) + println("Adding $(con)") + MOI.submit(model, MOI.LazyConstraint(cb_data), con) + end + return + end + set_attribute(model, MOI.LazyConstraintCallback(), my_callback_function) + optimize!(model) + assert_is_solved_and_feasible(model) + Test.@test lazy_called + Test.@test value(x) == 1 + Test.@test value(y) == 2 + println("Optimal solution (x, y) = ($(value(x)), $(value(y)))") + return +end + +example_lazy_constraint() +```` + +## User-cuts + +An example using a user-cut callback. + +````@example callbacks +function example_user_cut_constraint() + Random.seed!(1) + N = 30 + item_weights, item_values = rand(N), rand(N) + 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)) + callback_called = false + function my_callback_function(cb_data) + callback_called = true + x_vals = callback_value.(Ref(cb_data), x) + accumulated = sum(item_weights[i] for i in 1:N if x_vals[i] > 1e-4) + println("Called with accumulated = $(accumulated)") + n_terms = sum(1 for i in 1:N if x_vals[i] > 1e-4) + if accumulated > 10 + con = @build_constraint( + sum(x[i] for i in 1:N if x_vals[i] > 0.5) <= n_terms - 1 + ) + println("Adding $(con)") + MOI.submit(model, MOI.UserCut(cb_data), con) + end + end + set_attribute(model, MOI.UserCutCallback(), my_callback_function) + optimize!(model) + assert_is_solved_and_feasible(model) + Test.@test callback_called + @show callback_called + return +end + +example_user_cut_constraint() +```` + +## Heuristic solutions + +An example using a heuristic solution callback. + +````@example callbacks +function example_heuristic_solution() + Random.seed!(1) + N = 30 + item_weights, item_values = rand(N), rand(N) + 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)) + callback_called = false + function my_callback_function(cb_data) + callback_called = true + x_vals = callback_value.(Ref(cb_data), x) + ret = + MOI.submit(model, MOI.HeuristicSolution(cb_data), x, floor.(x_vals)) + println("Heuristic solution status = $(ret)") + Test.@test ret in ( + MOI.HEURISTIC_SOLUTION_ACCEPTED, + MOI.HEURISTIC_SOLUTION_REJECTED, + ) + end + set_attribute(model, MOI.HeuristicCallback(), my_callback_function) + optimize!(model) + assert_is_solved_and_feasible(model) + Test.@test callback_called + return +end + +example_heuristic_solution() +```` + +## Gurobi solver-dependent callback + +An example using Gurobi's solver-dependent callback. + +````@example callbacks +function example_solver_dependent_callback() + model = direct_model(Gurobi.Optimizer()) + @variable(model, 0 <= x <= 2.5, Int) + @variable(model, 0 <= y <= 2.5, Int) + @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 + 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) + MOI.submit(model, MOI.LazyConstraint(cb_data), con) + elseif y_val + x_val > 3 + 1e-6 + 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 + # 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 termination_status(model) == MOI.INTERRUPTED + return +end + +example_solver_dependent_callback() +```` + From 9989f29de669221a08d26fd78c6e60d977b83ddc Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 6 Jan 2026 10:21:22 +1300 Subject: [PATCH 3/9] Update --- docs/src/manual/containers.md | 2 +- docs/src/tutorials/linear/callbacks.md | 213 +------------------------ 2 files changed, 3 insertions(+), 212 deletions(-) diff --git a/docs/src/manual/containers.md b/docs/src/manual/containers.md index 999b3c09a6e..146846099f2 100644 --- a/docs/src/manual/containers.md +++ b/docs/src/manual/containers.md @@ -138,7 +138,7 @@ julia> DataFrames.DataFrame(table) 6 │ 2 3 (2, 3) ``` -## ## [DenseAxisArray](@id manual_dense_axis_array) +## [DenseAxisArray](@id manual_dense_axis_array) A [`Containers.DenseAxisArray`](@ref) is created when the index sets are rectangular, but not of the form `1:n`. The index sets can be of any type. diff --git a/docs/src/tutorials/linear/callbacks.md b/docs/src/tutorials/linear/callbacks.md index aa444b4d201..39f5dc4b86e 100644 --- a/docs/src/tutorials/linear/callbacks.md +++ b/docs/src/tutorials/linear/callbacks.md @@ -7,214 +7,5 @@ EditURL = "callbacks.jl" _This tutorial was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl)._ [_Download the source as a `.jl` file_](callbacks.jl). -The purpose of the tutorial is to demonstrate the various solver-independent -and solver-dependent callbacks that are supported by JuMP. - -The tutorial uses the following packages: - -````@example callbacks -using JuMP -import Gurobi -import Random -import Test -```` - -!!! info - This tutorial uses the [MathOptInterface](@ref moi_documentation) API. - By default, JuMP exports the `MOI` symbol as an alias for the - MathOptInterface.jl package. We recommend making this more explicit in - your code by adding the following lines: - ```julia - import MathOptInterface as MOI - ``` - -## Lazy constraints - -An example using a lazy constraint callback. - -````@example callbacks -function example_lazy_constraint() - 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) - lazy_called = false - function my_callback_function(cb_data) - lazy_called = true - x_val = callback_value(cb_data, x) - y_val = callback_value(cb_data, y) - println("Called from (x, y) = ($x_val, $y_val)") - status = callback_node_status(cb_data, model) - if status == MOI.CALLBACK_NODE_STATUS_FRACTIONAL - println(" - Solution is integer infeasible!") - elseif status == MOI.CALLBACK_NODE_STATUS_INTEGER - println(" - Solution is integer feasible!") - else - @assert status == MOI.CALLBACK_NODE_STATUS_UNKNOWN - println(" - I don't know if the solution is integer feasible :(") - end - 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 <= 3) - println("Adding $(con)") - MOI.submit(model, MOI.LazyConstraint(cb_data), con) - end - return - end - set_attribute(model, MOI.LazyConstraintCallback(), my_callback_function) - optimize!(model) - assert_is_solved_and_feasible(model) - Test.@test lazy_called - Test.@test value(x) == 1 - Test.@test value(y) == 2 - println("Optimal solution (x, y) = ($(value(x)), $(value(y)))") - return -end - -example_lazy_constraint() -```` - -## User-cuts - -An example using a user-cut callback. - -````@example callbacks -function example_user_cut_constraint() - Random.seed!(1) - N = 30 - item_weights, item_values = rand(N), rand(N) - 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)) - callback_called = false - function my_callback_function(cb_data) - callback_called = true - x_vals = callback_value.(Ref(cb_data), x) - accumulated = sum(item_weights[i] for i in 1:N if x_vals[i] > 1e-4) - println("Called with accumulated = $(accumulated)") - n_terms = sum(1 for i in 1:N if x_vals[i] > 1e-4) - if accumulated > 10 - con = @build_constraint( - sum(x[i] for i in 1:N if x_vals[i] > 0.5) <= n_terms - 1 - ) - println("Adding $(con)") - MOI.submit(model, MOI.UserCut(cb_data), con) - end - end - set_attribute(model, MOI.UserCutCallback(), my_callback_function) - optimize!(model) - assert_is_solved_and_feasible(model) - Test.@test callback_called - @show callback_called - return -end - -example_user_cut_constraint() -```` - -## Heuristic solutions - -An example using a heuristic solution callback. - -````@example callbacks -function example_heuristic_solution() - Random.seed!(1) - N = 30 - item_weights, item_values = rand(N), rand(N) - 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)) - callback_called = false - function my_callback_function(cb_data) - callback_called = true - x_vals = callback_value.(Ref(cb_data), x) - ret = - MOI.submit(model, MOI.HeuristicSolution(cb_data), x, floor.(x_vals)) - println("Heuristic solution status = $(ret)") - Test.@test ret in ( - MOI.HEURISTIC_SOLUTION_ACCEPTED, - MOI.HEURISTIC_SOLUTION_REJECTED, - ) - end - set_attribute(model, MOI.HeuristicCallback(), my_callback_function) - optimize!(model) - assert_is_solved_and_feasible(model) - Test.@test callback_called - return -end - -example_heuristic_solution() -```` - -## Gurobi solver-dependent callback - -An example using Gurobi's solver-dependent callback. - -````@example callbacks -function example_solver_dependent_callback() - model = direct_model(Gurobi.Optimizer()) - @variable(model, 0 <= x <= 2.5, Int) - @variable(model, 0 <= y <= 2.5, Int) - @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 - 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) - MOI.submit(model, MOI.LazyConstraint(cb_data), con) - elseif y_val + x_val > 3 + 1e-6 - 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 - # 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 termination_status(model) == MOI.INTERRUPTED - return -end - -example_solver_dependent_callback() -```` - +This page is a placeholder that appears only if the documentation is built from +a fork. From 9fcb31bb4e8ffabfb743a8368382250b5c788f7b Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 6 Jan 2026 14:33:30 +1300 Subject: [PATCH 4/9] Update --- docs/src/tutorials/applications/web_app.jl | 2 +- docs/src/tutorials/linear/callbacks.md | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/src/tutorials/applications/web_app.jl b/docs/src/tutorials/applications/web_app.jl index 4d90fe619e1..f9dfaf2f656 100644 --- a/docs/src/tutorials/applications/web_app.jl +++ b/docs/src/tutorials/applications/web_app.jl @@ -98,7 +98,7 @@ function setup_server(host, port) ## Log details about the exception server-side @info "Unhandled exception: $err" ## Return a response to the client - return HTTP.Response(500, "internal error") + return HTTP.Response(500, "internal error: $err") end end return server diff --git a/docs/src/tutorials/linear/callbacks.md b/docs/src/tutorials/linear/callbacks.md index 39f5dc4b86e..7e5cbdd625d 100644 --- a/docs/src/tutorials/linear/callbacks.md +++ b/docs/src/tutorials/linear/callbacks.md @@ -4,8 +4,5 @@ EditURL = "callbacks.jl" # [Callbacks](@id callbacks_tutorial) -_This tutorial was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl)._ -[_Download the source as a `.jl` file_](callbacks.jl). - This page is a placeholder that appears only if the documentation is built from a fork. From 3076ccc3f4e147cbb413f679f1ba4c2e0b8e079d Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 6 Jan 2026 16:45:58 +1300 Subject: [PATCH 5/9] Update --- docs/Project.toml | 2 +- docs/make.jl | 29 ++++++++++++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index cb5e786413f..0a083c9a3e0 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -57,7 +57,7 @@ DataFrames = "1.8.1" DifferentiationInterface = "0.7.13" DimensionalData = "=0.29.25" Distributions = "0.25.123" -Documenter = "=1.15.0" +Documenter = "=1.16.1" DocumenterCitations = "1.4.1" Dualization = "0.7.1" Enzyme = "0.13.112" diff --git a/docs/make.jl b/docs/make.jl index 4041f700288..dc68c59892e 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -560,17 +560,24 @@ function _add_moi_pages() end end end - # Fix `# Infeasibility certificates` in moi/background/infeasibility_certificates.md - filename = joinpath(moi_dir, "background", "infeasibility_certificates.md") - contents = read(filename, String) - id = "# [Infeasibility certificates](@id moi_infeasibility_certificates)" - contents = replace(contents, r"^# Infeasibility certificates$"m => id) - write(filename, contents) - # Fix `JSON.Object` in moi/submodules/FileFormats/overview.md - # This can be removed once we support JSON@1 in the documentation - filename = joinpath(moi_dir, "submodules", "FileFormats", "overview.md") - contents = read(filename, String) - write(filename, replace(contents, "JSON.Object" => "Dict")) + for (filename, replacements) in [ + "background/infeasibility_certificates.md" => [ + r"^# Infeasibility certificates$"m => "# [Infeasibility certificates](@id moi_infeasibility_certificates)", + ], + "reference/models.md" => ["# ResultStatusCode" => "# Result Status"], + # This can be removed once we support JSON@1 in the documentation + "submodules/FileFormats/overview.md" => ["JSON.Object" => "Dict"], + # These can be removed once we support MOI@1.48.1 or later + "reference/models.md" => ["# ResultStatusCode" => "# Result Status"], + "submodules/Nonlinear/SymbolicAD.md" => [ + "# `simplify`" => "# [`simplify`](@id symbolic_ad_manual_simplify)", + "# `variables`" => "# [`variables`](@id symbolic_ad_manual_variables)", + "# `derivative`" => "# [`derivative`](@id symbolic_ad_manual_derivative)", + ], + ] + filename = joinpath(moi_dir, filename) + write(filename, replace(read(filename, String), replacements...)) + end return end From fb7ac4e5ff800db9b396506b9d5e5edbc2bb0787 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 7 Jan 2026 09:39:45 +1300 Subject: [PATCH 6/9] Update --- docs/src/tutorials/applications/web_app.jl | 36 ++++++++++------------ 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/docs/src/tutorials/applications/web_app.jl b/docs/src/tutorials/applications/web_app.jl index f9dfaf2f656..990dcbfd059 100644 --- a/docs/src/tutorials/applications/web_app.jl +++ b/docs/src/tutorials/applications/web_app.jl @@ -84,33 +84,29 @@ end # in HTTP.jl. We use an explicit `Sockets.listen` so we have manual control of # when we shutdown the server. +## Extend the server by adding other endpoints here. +_ROUTES = Dict{String,Function}("/api/solve" => serve_solve) + +function serve_request(request::HTTP.Request) + if !haskey(_ROUTES, request.target) + return HTTP.Response(404, "target $(request.target) not found") + end + try + return _ROUTES[request.target](request)::HTTP.Response + catch err + @info "Unhandled exception: $err" + return HTTP.Response(500, "internal error: $err") + end +end + function setup_server(host, port) server = HTTP.Sockets.listen(host, port) HTTP.serve!(host, port; server = server) do request - try - ## Extend the server by adding other endpoints here. - if request.target == "/api/solve" - return serve_solve(request) - else - return HTTP.Response(404, "target $(request.target) not found") - end - catch err - ## Log details about the exception server-side - @info "Unhandled exception: $err" - ## Return a response to the client - return HTTP.Response(500, "internal error: $err") - end + return fetch(Threads.@spawn serve_request(request)) end return server end -# !!! warning -# HTTP.jl does not serve requests on a separate thread. Therefore, a -# long-running job will block the main thread, preventing concurrent users from -# submitting requests. To work-around this, read [HTTP.jl issue 798](https://github.com/JuliaWeb/HTTP.jl/issues/798) -# or watch [Building Microservices and Applications in Julia](https://www.youtube.com/watch?v=uLhXgt_gKJc&t=9543s) -# from JuliaCon 2020. - server = setup_server(HTTP.ip"127.0.0.1", 8080) # ## The client side From 6ef5df70ff224c2b0427cdd7f2af286e52a8eb6b Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 7 Jan 2026 11:18:17 +1300 Subject: [PATCH 7/9] Update --- docs/src/tutorials/applications/web_app.jl | 38 ++++++++-------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/docs/src/tutorials/applications/web_app.jl b/docs/src/tutorials/applications/web_app.jl index 990dcbfd059..23f6a7a8998 100644 --- a/docs/src/tutorials/applications/web_app.jl +++ b/docs/src/tutorials/applications/web_app.jl @@ -81,33 +81,23 @@ function serve_solve(request::HTTP.Request) end # Finally, we need an HTTP server. There are a variety of ways you can do this -# in HTTP.jl. We use an explicit `Sockets.listen` so we have manual control of -# when we shutdown the server. - -## Extend the server by adding other endpoints here. -_ROUTES = Dict{String,Function}("/api/solve" => serve_solve) - -function serve_request(request::HTTP.Request) - if !haskey(_ROUTES, request.target) - return HTTP.Response(404, "target $(request.target) not found") - end - try - return _ROUTES[request.target](request)::HTTP.Response - catch err - @info "Unhandled exception: $err" - return HTTP.Response(500, "internal error: $err") - end -end - -function setup_server(host, port) - server = HTTP.Sockets.listen(host, port) - HTTP.serve!(host, port; server = server) do request - return fetch(Threads.@spawn serve_request(request)) +# in HTTP.jl. Here's one way. We wrap each route in a function that runs the +# function in a separate thread and catches any unhandled exceptions. + +function wrap_serve_request(serve_fn::Function) + function serve_request(request::HTTP.Request) + task = Threads.@spawn try + serve_fn(request) + catch err + HTTP.Response(500, "internal error: $err") + end + return fetch(task) end - return server end -server = setup_server(HTTP.ip"127.0.0.1", 8080) +router = HTTP.Router() +HTTP.register!(router, "/api/solve", wrap_serve_request(serve_solve)) +server = HTTP.serve!(router, HTTP.ip"127.0.0.1", 8080) # ## The client side From de3cdab64af0e9d0820d51b65680f631070d763f Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 7 Jan 2026 12:20:04 +1300 Subject: [PATCH 8/9] Update --- docs/src/tutorials/applications/web_app.jl | 29 ++++++++++------------ 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/src/tutorials/applications/web_app.jl b/docs/src/tutorials/applications/web_app.jl index 23f6a7a8998..8d0997a6ea6 100644 --- a/docs/src/tutorials/applications/web_app.jl +++ b/docs/src/tutorials/applications/web_app.jl @@ -71,23 +71,16 @@ endpoint_solve(Dict{String,Any}("lower_bound" => 1.2)) endpoint_solve(Dict{String,Any}()) -# For a second function, we need a function that accepts an `HTTP.Request` -# object and returns an `HTTP.Response` object. +# We now need to turn each endpoint into a function that accepts an +# `HTTP.Request`, parses the JSON input, runs the endpoint, converts the result +# to JSON, and returns an `HTTP.Response`. In addition, the computation is +# handled in a separate thread, and we catch any unhandled exceptions. -function serve_solve(request::HTTP.Request) - data = JSON.parse(String(request.body)) - solution = endpoint_solve(data) - return HTTP.Response(200, JSON.json(solution)) -end - -# Finally, we need an HTTP server. There are a variety of ways you can do this -# in HTTP.jl. Here's one way. We wrap each route in a function that runs the -# function in a separate thread and catches any unhandled exceptions. - -function wrap_serve_request(serve_fn::Function) - function serve_request(request::HTTP.Request) +function wrap_endpoint(endpoint::Function) + function serve_request(request::HTTP.Request)::HTTP.Response task = Threads.@spawn try - serve_fn(request) + ret = request.body |> String |> JSON.parse |> endpoint |> JSON.json + HTTP.Response(200, ret) catch err HTTP.Response(500, "internal error: $err") end @@ -95,8 +88,12 @@ function wrap_serve_request(serve_fn::Function) end end +# Finally, we need an HTTP server. There are a variety of ways you can do this +# in HTTP.jl. Here's one way: + router = HTTP.Router() -HTTP.register!(router, "/api/solve", wrap_serve_request(serve_solve)) +## Register other routes as needed +HTTP.register!(router, "/api/solve", wrap_endpoint(endpoint_solve)) server = HTTP.serve!(router, HTTP.ip"127.0.0.1", 8080) # ## The client side From 13c72c09f1bdb8536d3a5d83ce31f02ffd5c56b4 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 7 Jan 2026 12:21:55 +1300 Subject: [PATCH 9/9] Update --- .vale.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.vale.ini b/.vale.ini index e9d218b1276..c70761a9754 100644 --- a/.vale.ini +++ b/.vale.ini @@ -63,6 +63,9 @@ Google.Quotes = NO Google.FirstPerson = NO Vale.Spelling = NO +[docs/src/packages/MultiObjectiveAlgorithms.md] +Vale.Spelling = NO + [docs/src/packages/Optim.md] Google.EmDash = NO