From 2ae63db5c5238be2dababcc2a77c75e6faa6b3b8 Mon Sep 17 00:00:00 2001 From: Guilherme Bodin <32756941+guilhermebodin@users.noreply.github.com> Date: Mon, 10 May 2021 15:34:11 -0300 Subject: [PATCH] Add documentation with Documenter.jl (#107) --- .github/workflows/documentation.yml | 23 +++++ .gitignore | 3 +- Project.toml | 2 +- docs/Project.toml | 2 +- docs/make.jl | 3 +- docs/src/examples.md | 1 - docs/src/index.md | 8 ++ docs/src/manual.md | 127 ++++++++++++++++++++++++++++ 8 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/documentation.yml delete mode 100644 docs/src/examples.md diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 00000000..5a642982 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,23 @@ +name: Documentation +on: + push: + branches: [master] + tags: '*' + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@latest + with: + # Build documentation on Julia 1.0 + version: '1.0' + - name: Install dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - name: Build and deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key + run: julia --project=docs/ docs/make.jl \ No newline at end of file diff --git a/.gitignore b/.gitignore index 83d89f72..8c536c03 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.jl.*.cov *.jl.mem deps/deps.jl -Manifest.toml \ No newline at end of file +Manifest.toml +docs/build \ No newline at end of file diff --git a/Project.toml b/Project.toml index 981e2044..daadb654 100644 --- a/Project.toml +++ b/Project.toml @@ -21,8 +21,8 @@ julia = "1" Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" QuadraticToBinary = "014a38d5-7acb-4e20-b6c0-4fe5c2344fd1" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" SCIP = "82193955-e24f-5292-bf16-6f2c5261a85f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test", "Cbc", "Ipopt", "QuadraticToBinary", "SCIP"] diff --git a/docs/Project.toml b/docs/Project.toml index 62c286eb..e6b1b0a3 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,4 +2,4 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" [compat] -Documenter = "~0.24" \ No newline at end of file +Documenter = "~0.26" \ No newline at end of file diff --git a/docs/make.jl b/docs/make.jl index 11815433..a1e71a61 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -9,8 +9,7 @@ makedocs( authors = "Joaquim Garcia", pages = [ "Home" => "index.md", - "manual.md", - "examples.md" + "manual.md" ] ) diff --git a/docs/src/examples.md b/docs/src/examples.md deleted file mode 100644 index 1b976a9b..00000000 --- a/docs/src/examples.md +++ /dev/null @@ -1 +0,0 @@ -# Examples \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md index 22c1d7e3..8ca352fc 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1 +1,9 @@ # BilevelJuMP.jl Documentation + +## Introduction + +BilevelJuMP is a package for modeling and solving bilevel optimization problems in Julia. As an extension of the JuMP package, BilevelJuMP allows users to employ the usual JuMP syntax with minor modifications to describe the problem and query solutions. + +BilevelJuMP is built on top of [MathOptInterface](https://github.com/JuMP-dev/MathOptInterface.jl) and makes strong use of its features to reformulate the problem as a single level problem and solve it with available MIP, NLP, and other solvers. + +The currently available methods are based on re-writing the problem using the KKT conditions of the lower level. For that we make strong use of [Dualization.jl](https://github.com/JuMP-dev/Dualization.jl) \ No newline at end of file diff --git a/docs/src/manual.md b/docs/src/manual.md index 39021136..ed8cd637 100644 --- a/docs/src/manual.md +++ b/docs/src/manual.md @@ -1 +1,128 @@ # Manual + +## Example + +```julia +using JuMP, BilevelJuMP, Cbc + +model = BilevelModel(Cbc.Optimizer, mode = BilevelJuMP.SOS1Mode()) + +@variable(Lower(model), x) +@variable(Upper(model), y) + +@objective(Upper(model), Min, 3x + y) +@constraints(Upper(model), begin + x <= 5 + y <= 8 + y >= 0 +end) + +@objective(Lower(model), Min, -x) +@constraints(Lower(model), begin + x + y <= 8 + 4x + y >= 8 + 2x + y <= 13 + 2x - 7y <= 0 +end) + +optimize!(model) + +objective_value(model) # = 3 * (3.5 * 8/15) + 8/15 # = 6.13... +value(x) # = 3.5 * 8/15 # = 1.86... +value(y) # = 8/15 # = 0.53... +``` + +The option `BilevelJuMP.SOS1Mode()` indicates that the solution method used +will be a KKT reformulation emplying SOS1 to model complementarity constraints +and solve the problem with MIP solvers (Cbc, Xpress, Gurobi, CPLEX, SCIP). + +Alternatively, the option `BilevelJuMP.IndicatorMode()` is almost equivalent to +the previous. The main difference is that it relies on Indicator constraints +instead. This kind of constraints is available in some MIP solvers. + +A third and classic option it the `BilevelJuMP.FortunyAmatMcCarlMode()`, which +relies on the Fortuny-Amat and McCarl big-M method that requires a MIP solver +with very basic functionality, i.e., just binary variables are needed. +The main drawback of this method is that one must provide bounds for all primal +and dual variables. However, if the bounds are good, this method can be more +efficient than the previous. Bound hints to compute the big-Ms can be passed +with the methods: `set_primal_(upper\lower)_bound_hint(variable, bound)`, for primals; +and `set_dual_(upper\lower)_bound(constraint, bound)` for duals. +We can also call `FortunyAmatMcCarlMode(primal_big_M = vp, dual_big_M = vd)`, +where `vp` and `vd` are, repspectively, the big M fallback values for primal +and dual variables, these are used when some variables have no given bounds, +otherwise the given bounds are used instead. + +Another option is `BilevelJuMP.ProductMode()` that reformulates the +complementarity constraints as products so that the problem can be solved by +NLP (Ipopt, KNITRO) solvers or even MIP solvers with the aid of binary +expansions +(see [QuadraticToBinary.jl](https://github.com/joaquimg/QuadraticToBinary.jl)). +Note that binary expansions require variables to have upper and lower bounds. + +Finally, one can use `BilevelJuMP.MixedMode(default = mode)` where `mode` is one +of the other modes described above. With this method it is possible to set +complementarity reformulations per constraint with `BilevelJuMP.set_mode(ctr, mode)`. + +An alternative to complementarity constraint reformulation is the Strong Duality +reformulation which add the constraint enforcing primal dual equality. The option +is `BilevelJuMP.StrongDualityMode(eps)` where `eps` is the tolance on the enforcing +constraint. + +### Note on [QuadraticToBinary.jl](https://github.com/joaquimg/QuadraticToBinary.jl) + +[QuadraticToBinary.jl](https://github.com/joaquimg/QuadraticToBinary.jl) is a +package that converts quadratic terms in constraints and objective. To do so +the pack acts like a solver on top of the real solver and most data is forwarded +directly to the solver itself. For many solvers it is enough to use: + +```julia +SOLVER = Xpress.Optimizer() +Q_SOLVER = QuadraticToBinary.Optimizer{Float64}(SOLVER) +BilevelModel(Q_SOLVER, mode = BilevelJuMP.ProductMode(1e-5)) +``` + +However, this might lead to some solver not supporting certain functionality like Cbc. +In this case we need to: + +```julia +SOLVER = Cbc.Optimizer() +CACHED_SOLVER = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), SOLVER) +Q_SOLVER = QuadraticToBinary.Optimizer{Float64}(CACHED_SOLVER) +BilevelModel(()->Q_SOLVER, mode = BilevelJuMP.ProductMode(1e-5)) +``` +Note that we used `()->Q_SOLVER` instead of just `Q_SOLVER` because `BilevelModel` +requires as constructor and not an instance of an object. + +## Advanced Features + +### Lower level dual variables + +Suppose you have a constraint `b` in the lower level: + +```julia +@constraint(Lower(model), b, ...) +``` + +It is possible to access the dual variable of `b` to use it in the upper level: + +```julia +@variable(Upper(model), lambda, DualOf(b)) +``` + +### Conic lower level + +BilevelJuMP allows users to write conic models in the lower level. However, +solving this kind of problems is much harder and requires complex solution +methods. Mosek's Conic MIP can be used with the aid of +[QuadraticToBinary.jl](https://github.com/joaquimg/QuadraticToBinary.jl). + +It is also possible to solve Second Order Cone constrained models with Ipopt. +In this case we need to add a special, non-standard bridge, to Ipopt as follows: + +```julia +IPO_OPT = Ipopt.Optimizer(print_level=0) +IPO = MOI.Bridges.Constraint.SOCtoNonConvexQuad{Float64}(IPO_OPT) +BilevelModel(()->IPO, mode = BilevelJuMP.ProductMode(1e-5)) +```