-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from COBREXA/sew-milp
Add ability to constraint variables to binary or integer values.
- Loading branch information
Showing
6 changed files
with
147 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
|
||
# # Example: Mixed integer optimization (MILP) | ||
# | ||
# This example demonstrates the extension of `ConstraintTree` bounds structures | ||
# to accomodate new kinds of problems. In particular, we create a new kind of | ||
# `Bound` that is restricting the value to be a full integer, and then solve a | ||
# geometric problem with that. | ||
|
||
# ## Creating a custom bound | ||
# | ||
# All bounds contained in constraints are subtypes of the abstract | ||
# [`ConstraintTrees.Bound`](@ref). These include | ||
# [`ConstraintTrees.EqualTo`](@ref) and [`ConstraintTrees.Between`](@ref), but | ||
# the types can be extended as necessary, given the final rewriting of the | ||
# constraint system to JuMP can handle the new bounds. | ||
# | ||
# Let's make a small "marker" bound for something that needs to be integer-ish, | ||
# between 2 integers: | ||
|
||
import ConstraintTrees as C | ||
|
||
mutable struct IntegerFromTo <: C.Bound | ||
from::Int | ||
to::Int | ||
end | ||
|
||
# We can now write e.g. a bound on the number on a thrown six-sided die as | ||
# follows: | ||
|
||
IntegerFromTo(1, 6) | ||
|
||
# ...and include this bound in constraints and variables: | ||
|
||
dice_system = | ||
C.variables(keys = [:first_dice, :second_dice], bounds = [IntegerFromTo(1, 6)]) | ||
|
||
# Now the main thing that is left is to be able to translate this bound to JuMP | ||
# for solving. We can slightly generalize our constraint-translation system | ||
# from the previous examples for this purpose, by separating out the functions | ||
# that create the constraints: | ||
|
||
import JuMP | ||
|
||
function jump_constraint(m, x, v::C.Value, b::C.EqualTo) | ||
JuMP.@constraint(m, C.substitute(v, x) == b.equal_to) | ||
end | ||
|
||
function jump_constraint(m, x, v::C.Value, b::C.Between) | ||
isinf(b.lower) || JuMP.@constraint(m, C.substitute(v, x) >= b.lower) | ||
isinf(b.upper) || JuMP.@constraint(m, C.substitute(v, x) <= b.upper) | ||
end | ||
|
||
# JuMP does not support direct integrality constraints, so we need to make a | ||
# small disgression with a slack variable: | ||
function jump_constraint(m, x, v::C.Value, b::IntegerFromTo) | ||
var = JuMP.@variable(m, integer = true) | ||
JuMP.@constraint(m, var >= b.from) | ||
JuMP.@constraint(m, var <= b.to) | ||
JuMP.@constraint(m, C.substitute(v, x) == var) | ||
end | ||
|
||
function milp_optimized_vars(cs::C.ConstraintTree, objective::C.Value, optimizer) | ||
model = JuMP.Model(optimizer) | ||
JuMP.@variable(model, x[1:C.var_count(cs)]) | ||
JuMP.@objective(model, JuMP.MAX_SENSE, C.substitute(objective, x)) | ||
function add_constraint(c::C.Constraint) | ||
isnothing(c.bound) || jump_constraint(model, x, c.value, c.bound) | ||
end | ||
function add_constraint(c::C.ConstraintTree) | ||
add_constraint.(values(c)) | ||
end | ||
add_constraint(cs) | ||
JuMP.set_silent(model) | ||
JuMP.optimize!(model) | ||
JuMP.value.(model[:x]) | ||
end | ||
|
||
# Let's try to solve a tiny system with the dice first. What's the best value | ||
# we can throw if the dice are thrown at least 1.5 points apart? | ||
|
||
dice_system *= | ||
:points_distance^C.Constraint( | ||
dice_system.first_dice.value - dice_system.second_dice.value, | ||
C.Between(1.5, Inf), | ||
) | ||
|
||
# For solving, we use GLPK (it has MILP capabilities). | ||
import GLPK | ||
dices_thrown = C.constraint_values( | ||
dice_system, | ||
milp_optimized_vars( | ||
dice_system, | ||
dice_system.first_dice.value + dice_system.second_dice.value, | ||
GLPK.Optimizer, | ||
), | ||
) | ||
|
||
@test isapprox(dices_thrown.first_dice, 6.0) #src | ||
@test isapprox(dices_thrown.second_dice, 4.0) #src | ||
|
||
# ## A more realistic example with geometry | ||
# | ||
# Let's find the size of the smallest right-angled triangle with integer side | ||
# sizes (aka a Pythagorean triple). | ||
|
||
vars = C.variables(keys = [:a, :b, :c], bounds = (IntegerFromTo(1, 100),)) | ||
|
||
# For simpliclty, we make a shortcut for "values" in all variables: | ||
v = C.tree_map(vars, C.value, C.Value) | ||
|
||
# With that shortcut, the constraint tree constructs quite easily: | ||
triangle_system = | ||
:sides^vars * | ||
:circumference^C.Constraint(sum(values(v))) * | ||
:a_less_than_b^C.Constraint(v.b - v.a, (0, Inf)) * | ||
:b_less_than_c^C.Constraint(v.c - v.b, (0, Inf)) * | ||
:right_angled^C.Constraint(C.squared(v.a) + C.squared(v.b) - C.squared(v.c), 0.0) | ||
|
||
# We will need a solver that supports both quadratic and integer optimization: | ||
import SCIP | ||
triangle_sides = | ||
C.constraint_values( | ||
triangle_system, | ||
milp_optimized_vars( | ||
triangle_system, | ||
-triangle_system.circumference.value, | ||
SCIP.Optimizer, | ||
), | ||
).sides | ||
|
||
@test isapprox(triangle_sides.a, 3.0) #src | ||
@test isapprox(triangle_sides.b, 4.0) #src | ||
@test isapprox(triangle_sides.c, 5.0) #src |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8df256b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JuliaRegistrator register
8df256b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Registration pull request created: JuliaRegistries/General/97582
Tip: Release Notes
Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.
To add them here just re-invoke and the PR will be updated.
Tagging
After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.
This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via: