Example: Mixed integer optimization (MILP)
This example demonstrates the extension of ConstraintTree bounds structures to accommodate 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. These include ConstraintTrees.EqualTo and ConstraintTrees.Between, 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
endWe can now write e.g. a bound on the number on a thrown six-sided die as follows:
IntegerFromTo(1, 6)Main.var"Main".IntegerFromTo(1, 6)...and include this bound in constraints and variables:
dice_system = C.variables(keys = [:first_dice, :second_dice], bounds = IntegerFromTo(1, 6))ConstraintTree with 2 elements:
:first_dice => Constraint(LinearValue(#= ... =#), Main.var"Main".IntegerFromTo(1, 6))
:second_dice => Constraint(LinearValue(#= ... =#), Main.var"Main".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)
endjump_constraint (generic function with 2 methods)JuMP does not support direct integrality constraints, so we need to make a small digression 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.variable_count(cs)])
JuMP.@objective(model, JuMP.MAX_SENSE, C.substitute(objective, x))
C.traverse(cs) do c
isnothing(c.bound) || jump_constraint(model, x, c.value, c.bound)
end
JuMP.set_silent(model)
JuMP.optimize!(model)
JuMP.value.(model[:x])
endmilp_optimized_vars (generic function with 1 method)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),
)ConstraintTree with 3 elements:
:first_dice => Constraint(LinearValue(#= ... =#), Main.var"Main".IntegerFromTo(1, 6))
:points_distance => Constraint(LinearValue(#= ... =#), Between(1.5, Inf))
:second_dice => Constraint(LinearValue(#= ... =#), Main.var"Main".IntegerFromTo(1, 6))For solving, we use GLPK (it has MILP capabilities).
import GLPK
dices_thrown = C.substitute_values(
dice_system,
milp_optimized_vars(
dice_system,
dice_system.first_dice.value + dice_system.second_dice.value,
GLPK.Optimizer,
),
)Tree{Float64} with 3 elements:
:first_dice => 6.0
:points_distance => 2.0
:second_dice => 4.0A note on pretty-printing of custom extensions
By default, pretty-printing via pretty attempts to fall back to Base.show for any value which has no explicit overload of pretty. In particular, the bounds in our MILP system are formatted as follows:
C.pretty(dice_system)┬─first_dice: 1.0*x[1]; Main.var"Main".IntegerFromTo(1, 6)
├─points_distance: 1.0*x[1] + -1.0*x[2] ∈ [1.5, Inf]
╰─second_dice: 1.0*x[2]; Main.var"Main".IntegerFromTo(1, 6)To provide a prettier rendering, it is sufficient to provide a matching overload of pretty:
function C.pretty(io::IO, x::IntegerFromTo; style_args...)
print(io, " ∈ {$(x.from) … $(x.to)}")
end
C.pretty(dice_system)┬─first_dice: 1.0*x[1] ∈ {1 … 6}
├─points_distance: 1.0*x[1] + -1.0*x[2] ∈ [1.5, Inf]
╰─second_dice: 1.0*x[2] ∈ {1 … 6}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))ConstraintTree with 3 elements:
:a => Constraint(LinearValue(#= ... =#), Main.var"Main".IntegerFromTo(1, 100))
:b => Constraint(LinearValue(#= ... =#), Main.var"Main".IntegerFromTo(1, 100))
:c => Constraint(LinearValue(#= ... =#), Main.var"Main".IntegerFromTo(1, 100))For simpliclty, we make a shortcut for "values" in all variables:
v = C.map(C.value, vars, C.Value)Tree{Value} with 3 elements:
:a => LinearValue([1], [1.0])
:b => LinearValue([2], [1.0])
:c => LinearValue([3], [1.0])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)
C.pretty(triangle_system, format_variable = i -> ["", "A", "B", "C"][i+1])┬─a_less_than_b: -1.0A + 1.0B ∈ [0.0, Inf]
├─b_less_than_c: -1.0B + 1.0C ∈ [0.0, Inf]
├─circumference: 1.0A + 1.0B + 1.0C
├─right_angled: 1.0AA + 1.0BB + -1.0CC = 0.0
╰─sides
╰─┬─a: 1.0A ∈ {1 … 100}
├─b: 1.0B ∈ {1 … 100}
╰─c: 1.0C ∈ {1 … 100}We will need a solver that supports both quadratic and integer optimization:
import SCIP
triangle_sides = C.substitute_values(
triangle_system,
milp_optimized_vars(
triangle_system,
-triangle_system.circumference.value,
SCIP.Optimizer,
),
).sidesTree{Float64} with 3 elements:
:a => 3.0
:b => 4.0
:c => 5.0This page was generated using Literate.jl.